@dimina-kit/devtools 0.4.0-dev.20260702182435 → 0.4.0-dev.20260703101348
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 +5 -25
- package/dist/main/app/native-overview.d.ts +27 -0
- package/dist/main/app/native-overview.js +44 -0
- package/dist/main/index.bundle.js +1217 -735
- package/dist/main/ipc/bridge-router.d.ts +8 -0
- package/dist/main/ipc/bridge-router.js +526 -173
- package/dist/main/ipc/project-fs.js +39 -21
- package/dist/main/services/automation/handlers/app.js +143 -109
- 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/mcp/tools/simulator-tools.js +118 -97
- package/dist/main/services/network-forward/dispatch-batch.d.ts +22 -0
- package/dist/main/services/network-forward/dispatch-batch.js +21 -0
- package/dist/main/services/network-forward/index.js +37 -20
- 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/create-project-service.js +54 -34
- package/dist/main/services/projects/project-repository.js +2 -1
- package/dist/main/services/projects/types.d.ts +1 -32
- package/dist/main/services/service-console/console-api.js +36 -27
- package/dist/main/services/simulator-temp-files/disk.js +109 -86
- package/dist/main/services/views/simulator-session-policy.js +17 -0
- package/dist/main/services/workbench-coi-server.js +58 -45
- package/dist/main/services/workbench-context.d.ts +10 -0
- package/dist/main/services/workspace/workspace-service.js +12 -3
- package/dist/main/utils/logger.d.ts +5 -12
- package/dist/main/utils/logger.js +5 -45
- 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/instrumentation/wxml-extract.js +94 -62
- package/dist/preload/runtime/main-api-runner.d.ts +1 -1
- 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 +284 -565
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/preload/windows/simulator.cjs +154 -83
- package/dist/preload/windows/simulator.cjs.map +3 -3
- package/dist/preload/windows/simulator.js +154 -83
- package/dist/render-host/render-inspect.js +63 -52
- 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-Cr4EHo5w.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/appdata-accumulator.js +53 -48
- 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/open-in-editor-resource-path.d.ts +56 -0
- package/dist/shared/open-in-editor-resource-path.js +133 -0
- package/dist/shared/open-in-editor.d.ts +2 -9
- package/dist/shared/open-in-editor.js +8 -93
- 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 +20 -4
- package/dist/simulator/assets/{device-shell-CUl0ILfn.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/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +1 -0
- package/dist/vscode-workbench/assets/{dist-CS7SQP3K.js → dist-TpGpmMGi.js} +3 -3
- package/dist/vscode-workbench/assets/{iconv-lite-umd-D3q-1-o6.js → iconv-lite-umd-C9tiCAJa.js} +1 -1
- package/dist/vscode-workbench/assets/{index-zjigpZ25.js → index-DJ1HyMZ7.js} +20 -22
- package/dist/vscode-workbench/assets/{jschardet-Cu4ufeHq.js → jschardet-DE1jV513.js} +1 -1
- package/dist/vscode-workbench/index.html +1 -1
- package/package.json +6 -6
- package/dist/renderer/assets/index-B-Dqb7G0.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/renderer/assets/workbenchSettings-Bim9ol9R.js +0 -8
- package/dist/simulator/assets/simulator-Dz8iGcgy.js +0 -10
- package/dist/simulator/assets/simulator-mini-app-Bt639XL_.js +0 -2
- package/dist/vscode-workbench/assets/__vite-browser-external-B0DTNerG.js +0 -1
|
@@ -2,7 +2,7 @@ import { app, ipcMain, protocol, session as electronSession, webContents } from
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { pathToFileURL } from 'node:url';
|
|
4
4
|
import { BRIDGE_CHANNELS as C, SIMULATOR_EVENTS as E, deviceInfoToHostEnv } from '../../shared/bridge-channels.js';
|
|
5
|
-
import { isPersistentSimulatorApi } from '../../shared/simulator-api-metadata.js';
|
|
5
|
+
import { apiCallWatchdogMs, isPersistentSimulatorApi } from '../../shared/simulator-api-metadata.js';
|
|
6
6
|
import { devtoolsPackageRoot } from '../utils/paths.js';
|
|
7
7
|
import { createSessionListenerBag } from './session-listener-bag.js';
|
|
8
8
|
import { createDebugTap } from '@dimina-kit/electron-deck/main';
|
|
@@ -11,6 +11,7 @@ import { buildServiceHostSpawnUrl, createServiceHostWindow, navigateServiceHost,
|
|
|
11
11
|
import { ServiceHostPool } from '../services/service-host-pool/pool.js';
|
|
12
12
|
import { registerMiniappSessionConfigurator, SHARED_MINIAPP_PARTITION, } from '../services/views/miniapp-partition.js';
|
|
13
13
|
import { createConsoleForwarder } from '../services/console-forward/index.js';
|
|
14
|
+
import { createDiagnosticsBus } from '../services/diagnostics/index.js';
|
|
14
15
|
import { STORAGE_API_NAMES } from '../services/simulator-storage/index.js';
|
|
15
16
|
import { buildPageScrollScript } from './page-scroll.js';
|
|
16
17
|
import { createAppLifecycleController, } from './app-lifecycle.js';
|
|
@@ -55,6 +56,13 @@ function rewriteSourceMappingUrl(source, scriptUrl) {
|
|
|
55
56
|
return `${source.slice(0, lastIndex)}\n//# sourceMappingURL=${absolute}`;
|
|
56
57
|
}
|
|
57
58
|
const STACK_ID = 'stack_0';
|
|
59
|
+
/**
|
|
60
|
+
* Watchdog window a spawned session gets to reach `'running'` (root page
|
|
61
|
+
* `domReady`) before `handleSpawn`'s launch timer reports `'launch-failed'`
|
|
62
|
+
* with `code: 'timeout'`. Exported so tests can assert against the real
|
|
63
|
+
* constant instead of a magic number.
|
|
64
|
+
*/
|
|
65
|
+
export const LAUNCH_TIMEOUT_MS = 20_000;
|
|
58
66
|
/** Hard ceiling on the pre-warm pool, mirroring ServiceHostPool/doc §3.3. */
|
|
59
67
|
const PREWARM_MAX_POOL_SIZE = 4;
|
|
60
68
|
/**
|
|
@@ -92,8 +100,34 @@ function summarizeBridgeMsg(payload) {
|
|
|
92
100
|
const parts = [type, bridgeId ? `bridge=${bridgeId}` : undefined].filter(Boolean);
|
|
93
101
|
return parts.length > 0 ? parts.join(' ') : undefined;
|
|
94
102
|
}
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
// Same-appId matches prefer the MOST RECENT spawn (Maps preserve insertion
|
|
104
|
+
// order): after a respawn/reopen the newest session is the live one — the
|
|
105
|
+
// first match could be a just-superseded session mid-teardown. The loop must
|
|
106
|
+
// scan every session (no early break) so the LAST inserted match wins.
|
|
107
|
+
function findAppSessionByAppId(state, appId) {
|
|
108
|
+
let match;
|
|
109
|
+
for (const ap of state.appSessions.values())
|
|
110
|
+
if (ap.appId === appId)
|
|
111
|
+
match = ap;
|
|
112
|
+
return match;
|
|
113
|
+
}
|
|
114
|
+
// No appId hint and no workspace session to disambiguate. Picking by appId is
|
|
115
|
+
// impossible, so fall back to the most-recent spawn — but ONLY when every live
|
|
116
|
+
// session belongs to the same app. Same-appId multiples are a respawn/reopen
|
|
117
|
+
// in progress where the newest is the live one (insertion order = spawn
|
|
118
|
+
// order). Multiple DISTINCT appIds mean a previous project is mid-teardown
|
|
119
|
+
// alongside the new one; any pick is a guess that can resolve the WRONG
|
|
120
|
+
// project's content (the screenshot/inspect-stale-app bug class), so prefer
|
|
121
|
+
// null over a wrong guess.
|
|
122
|
+
function resolveFallbackAppSession(state) {
|
|
123
|
+
let last;
|
|
124
|
+
const distinctAppIds = new Set();
|
|
125
|
+
for (const ap of state.appSessions.values()) {
|
|
126
|
+
last = ap;
|
|
127
|
+
distinctAppIds.add(ap.appId);
|
|
128
|
+
}
|
|
129
|
+
return distinctAppIds.size <= 1 ? last : undefined;
|
|
130
|
+
}
|
|
97
131
|
/**
|
|
98
132
|
* Resolve the "current" app session: prefer an explicit appId, else the
|
|
99
133
|
* workspace's active project, else the only / most-recently-created session.
|
|
@@ -101,24 +135,15 @@ const API_CALL_TIMEOUT_MS = 5_000;
|
|
|
101
135
|
* rather than assuming a single session.
|
|
102
136
|
*/
|
|
103
137
|
function resolveCurrentApp(state, ctx, appId) {
|
|
104
|
-
// Same-appId matches prefer the MOST RECENT spawn (Maps preserve insertion
|
|
105
|
-
// order): after a respawn/reopen the newest session is the live one — the
|
|
106
|
-
// first match could be a just-superseded session mid-teardown.
|
|
107
138
|
if (appId) {
|
|
108
|
-
|
|
109
|
-
for (const ap of state.appSessions.values())
|
|
110
|
-
if (ap.appId === appId)
|
|
111
|
-
match = ap;
|
|
139
|
+
const match = findAppSessionByAppId(state, appId);
|
|
112
140
|
if (match)
|
|
113
141
|
return match;
|
|
114
142
|
}
|
|
115
143
|
const appInfo = ctx.workspace?.getSession?.()?.appInfo;
|
|
116
144
|
const activeAppId = appInfo?.appId;
|
|
117
145
|
if (activeAppId) {
|
|
118
|
-
|
|
119
|
-
for (const ap of state.appSessions.values())
|
|
120
|
-
if (ap.appId === activeAppId)
|
|
121
|
-
match = ap;
|
|
146
|
+
const match = findAppSessionByAppId(state, activeAppId);
|
|
122
147
|
if (match)
|
|
123
148
|
return match;
|
|
124
149
|
}
|
|
@@ -128,21 +153,7 @@ function resolveCurrentApp(state, ctx, appId) {
|
|
|
128
153
|
// while a close is in flight.
|
|
129
154
|
if (ctx.workspace?.isClosing?.())
|
|
130
155
|
return undefined;
|
|
131
|
-
|
|
132
|
-
// impossible, so fall back to the most-recent spawn — but ONLY when every live
|
|
133
|
-
// session belongs to the same app. Same-appId multiples are a respawn/reopen
|
|
134
|
-
// in progress where the newest is the live one (insertion order = spawn
|
|
135
|
-
// order). Multiple DISTINCT appIds mean a previous project is mid-teardown
|
|
136
|
-
// alongside the new one; any pick is a guess that can resolve the WRONG
|
|
137
|
-
// project's content (the screenshot/inspect-stale-app bug class), so prefer
|
|
138
|
-
// null over a wrong guess.
|
|
139
|
-
let last;
|
|
140
|
-
const distinctAppIds = new Set();
|
|
141
|
-
for (const ap of state.appSessions.values()) {
|
|
142
|
-
last = ap;
|
|
143
|
-
distinctAppIds.add(ap.appId);
|
|
144
|
-
}
|
|
145
|
-
return distinctAppIds.size <= 1 ? last : undefined;
|
|
156
|
+
return resolveFallbackAppSession(state);
|
|
146
157
|
}
|
|
147
158
|
export function installBridgeRouter(ctx) {
|
|
148
159
|
const state = {
|
|
@@ -338,13 +349,28 @@ export function installBridgeRouter(ctx) {
|
|
|
338
349
|
// Chrome DevTools (attached to the service host) prefixed `[视图]` — regardless
|
|
339
350
|
// of whether an automation client is connected. Automation now SUBSCRIBES to
|
|
340
351
|
// this forwarder instead of clobbering `ctx.guestConsole`.
|
|
341
|
-
|
|
352
|
+
// Authoritative diagnostics bus (see workbench-context.ts): the single place
|
|
353
|
+
// main-synthesized diagnostics are born. The forwarder subscribes to it so
|
|
354
|
+
// every diagnostic also lands in the owning session's embedded DevTools
|
|
355
|
+
// Console panel, not just the main-process log / automation subscribers.
|
|
356
|
+
// A caller (host embedding, or a test) may have already installed one on
|
|
357
|
+
// `ctx` before `installBridgeRouter` runs — respect it instead of
|
|
358
|
+
// clobbering it with a fresh bus; only a bus WE create here is ours to
|
|
359
|
+
// dispose on teardown.
|
|
360
|
+
const ownsDiagnosticsBus = ctx.diagnostics === undefined;
|
|
361
|
+
const diagnosticsBus = ctx.diagnostics ?? createDiagnosticsBus();
|
|
362
|
+
ctx.diagnostics = diagnosticsBus;
|
|
363
|
+
const consoleForwarder = createConsoleForwarder(bridgeHandle, diagnosticsBus);
|
|
342
364
|
ctx.consoleForwarder = consoleForwarder;
|
|
343
365
|
ctx.guestConsole = consoleForwarder;
|
|
344
366
|
ctx.registry.add(() => {
|
|
345
367
|
void consoleForwarder.dispose();
|
|
346
368
|
ctx.consoleForwarder = undefined;
|
|
347
369
|
ctx.guestConsole = undefined;
|
|
370
|
+
if (ownsDiagnosticsBus) {
|
|
371
|
+
diagnosticsBus.dispose();
|
|
372
|
+
ctx.diagnostics = undefined;
|
|
373
|
+
}
|
|
348
374
|
});
|
|
349
375
|
// DeviceShell → main: record the visible top-of-stack page bridgeId so the
|
|
350
376
|
// accessor above can resolve "the active page". Sender-validated against the
|
|
@@ -511,6 +537,64 @@ export function installBridgeRouter(ctx) {
|
|
|
511
537
|
});
|
|
512
538
|
}
|
|
513
539
|
// ── Spawn / Open / Close ─────────────────────────────────────────────────────
|
|
540
|
+
/** Clear a session's launch-timeout watchdog, if still armed. Idempotent. */
|
|
541
|
+
function clearLaunchTimer(ap) {
|
|
542
|
+
if (ap.launchTimer === null)
|
|
543
|
+
return;
|
|
544
|
+
clearTimeout(ap.launchTimer);
|
|
545
|
+
ap.launchTimer = null;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Arm the `LAUNCH_TIMEOUT_MS` watchdog for a freshly spawned session. Fires
|
|
549
|
+
* `'launch-failed'` (`code: 'timeout'`) exactly once, and only if the session
|
|
550
|
+
* is both still registered (not disposed) and has not already reached
|
|
551
|
+
* `'running'` — both settle paths clear the timer themselves, but a timer
|
|
552
|
+
* queued just before either lands could still fire on the old macrotask queue.
|
|
553
|
+
*/
|
|
554
|
+
function startLaunchTimer(state, ctx, ap) {
|
|
555
|
+
ap.launchTimer = setTimeout(() => {
|
|
556
|
+
ap.launchTimer = null;
|
|
557
|
+
if (ap.running)
|
|
558
|
+
return;
|
|
559
|
+
if (state.appSessions.get(ap.appSessionId) !== ap)
|
|
560
|
+
return;
|
|
561
|
+
const reason = `Service host did not report readiness within ${LAUNCH_TIMEOUT_MS}ms`;
|
|
562
|
+
ctx.diagnostics?.report({
|
|
563
|
+
severity: 'error',
|
|
564
|
+
code: 'launch-timeout',
|
|
565
|
+
message: reason,
|
|
566
|
+
appSessionId: ap.appSessionId,
|
|
567
|
+
});
|
|
568
|
+
pushRuntimeStatus(ctx, ap, { phase: 'launch-failed', code: 'timeout', reason });
|
|
569
|
+
}, LAUNCH_TIMEOUT_MS);
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Flip a session to `'running'` on its root page's first `domReady` — the
|
|
573
|
+
* single source-of-truth transition out of `'launching'`. Guarded by
|
|
574
|
+
* `ap.running` so a later re-render of the same root page (or any non-root
|
|
575
|
+
* page) never re-fires the notification or re-clears an already-cleared timer.
|
|
576
|
+
*/
|
|
577
|
+
function markSessionRunning(ctx, ap, page) {
|
|
578
|
+
if (!page.isRoot || ap.running)
|
|
579
|
+
return;
|
|
580
|
+
ap.running = true;
|
|
581
|
+
clearLaunchTimer(ap);
|
|
582
|
+
pushRuntimeStatus(ctx, ap, { phase: 'running' });
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* The single chokepoint for session runtime-status pushes. Every event
|
|
586
|
+
* carries the session's launch-round facts (appId + pageFallback) alongside
|
|
587
|
+
* the phase change — the renderer replaces its runtimeStatus wholesale, so an
|
|
588
|
+
* event that omitted a still-true fallback would silently blank the fallback
|
|
589
|
+
* banner the moment the phase advances past 'launching'.
|
|
590
|
+
*/
|
|
591
|
+
function pushRuntimeStatus(ctx, session, status) {
|
|
592
|
+
ctx.notify?.sessionRuntimeStatus?.({
|
|
593
|
+
appId: session.appId,
|
|
594
|
+
...status,
|
|
595
|
+
...(session.pageFallback ? { pageFallback: session.pageFallback } : {}),
|
|
596
|
+
});
|
|
597
|
+
}
|
|
514
598
|
async function handleSpawn(state, ctx, event, opts) {
|
|
515
599
|
const appId = opts.appId;
|
|
516
600
|
if (!appId)
|
|
@@ -557,10 +641,21 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
557
641
|
});
|
|
558
642
|
// app-config.json lives at `<base><appId>/<root>/app-config.json` on the dev
|
|
559
643
|
// server, or at the local server root for the fallback path.
|
|
560
|
-
const appConfig = await loadAppConfig(resourceServer ? resourceServer.baseUrl : `${resourceBaseUrl}${appId}/${root}
|
|
644
|
+
const appConfig = await loadAppConfig(resourceServer ? resourceServer.baseUrl : `${resourceBaseUrl}${appId}/${root}/`, ({ url, error }) => {
|
|
645
|
+
ctx.diagnostics?.report({
|
|
646
|
+
severity: 'error',
|
|
647
|
+
code: 'app-config-unreachable',
|
|
648
|
+
message: `app-config.json unreachable at ${url}: ${String(error)}`,
|
|
649
|
+
appSessionId,
|
|
650
|
+
});
|
|
651
|
+
});
|
|
561
652
|
const manifest = buildAppManifest(appConfig, pagePath);
|
|
562
|
-
const
|
|
563
|
-
|
|
653
|
+
const { resolvedPagePath, pageFallbackApplied } = resolveRootPagePath(manifest, pagePath);
|
|
654
|
+
if (pageFallbackApplied) {
|
|
655
|
+
reportPageNotFound(ctx, appSessionId, pagePath, resolvedPagePath);
|
|
656
|
+
}
|
|
657
|
+
const rootWindowConfig = resolvePageWindowConfig(appConfig, resolvedPagePath);
|
|
658
|
+
const isTab = isTabPage(appConfig, resolvedPagePath);
|
|
564
659
|
// Acquire a pre-warmed service-host window when pooling is enabled; otherwise
|
|
565
660
|
// construct one fresh (default). A pooled/fallback window is warmed on
|
|
566
661
|
// about:blank and must be navigated to the spawn URL below; the fresh path
|
|
@@ -568,13 +663,31 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
568
663
|
const usedPool = state.pool !== null;
|
|
569
664
|
let poolEntryId = null;
|
|
570
665
|
let serviceWindow;
|
|
666
|
+
// Shared spawn-navigation failure handler (fresh AND pooled paths): without
|
|
667
|
+
// it a loadURL rejection is fully swallowed — did-finish-load never fires and
|
|
668
|
+
// the only signal is the launch watchdog's late timeout instead of the cause.
|
|
669
|
+
const reportServiceHostNavigationFailed = (spawnUrl, err) => {
|
|
670
|
+
const message = `Failed to navigate service host to ${spawnUrl}: ${String(err)}`;
|
|
671
|
+
ctx.diagnostics?.report({
|
|
672
|
+
severity: 'error',
|
|
673
|
+
code: 'service-host-navigation-failed',
|
|
674
|
+
message,
|
|
675
|
+
appSessionId,
|
|
676
|
+
});
|
|
677
|
+
// The session registers after window construction; resolve it fresh so the
|
|
678
|
+
// fresh-path callback (created pre-registration) still clears the watchdog.
|
|
679
|
+
const ap = state.appSessions.get(appSessionId);
|
|
680
|
+
if (ap)
|
|
681
|
+
clearLaunchTimer(ap);
|
|
682
|
+
pushRuntimeStatus(ctx, ap ?? { appId, pageFallback: pageFallbackApplied ? { requested: pagePath, resolved: resolvedPagePath } : null }, { phase: 'launch-failed', code: 'service-host-navigation-failed', reason: message });
|
|
683
|
+
};
|
|
571
684
|
if (state.pool) {
|
|
572
685
|
const acquired = await state.pool.acquire(serviceHostSpec());
|
|
573
686
|
serviceWindow = acquired.win;
|
|
574
687
|
poolEntryId = acquired.entryId;
|
|
575
688
|
}
|
|
576
689
|
else {
|
|
577
|
-
|
|
690
|
+
const freshWindowOptions = {
|
|
578
691
|
bridgeId,
|
|
579
692
|
appId,
|
|
580
693
|
// Same (appId, projectPath) pair the simulator WCV uses, so this project's
|
|
@@ -584,12 +697,16 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
584
697
|
// awaits above would otherwise give the service host a different path than
|
|
585
698
|
// the simulator WCV was built with, splitting the partition.
|
|
586
699
|
projectPath: workspaceProjectPath || undefined,
|
|
587
|
-
pagePath,
|
|
700
|
+
pagePath: resolvedPagePath,
|
|
588
701
|
pkgRoot,
|
|
589
702
|
root,
|
|
590
703
|
resourceBaseUrl,
|
|
591
704
|
hostEnvSnapshot: hostEnv,
|
|
592
705
|
apiNamespaces,
|
|
706
|
+
};
|
|
707
|
+
serviceWindow = createServiceHostWindow({
|
|
708
|
+
...freshWindowOptions,
|
|
709
|
+
onLoadFailed: err => reportServiceHostNavigationFailed(buildServiceHostSpawnUrl(freshWindowOptions), err),
|
|
593
710
|
});
|
|
594
711
|
}
|
|
595
712
|
const appSession = {
|
|
@@ -614,11 +731,14 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
614
731
|
onServiceBoot: null,
|
|
615
732
|
listenerBag: createSessionListenerBag(),
|
|
616
733
|
registryHandle: null,
|
|
734
|
+
running: false,
|
|
735
|
+
launchTimer: null,
|
|
736
|
+
pageFallback: pageFallbackApplied ? { requested: pagePath, resolved: resolvedPagePath } : null,
|
|
617
737
|
};
|
|
618
738
|
const rootPage = {
|
|
619
739
|
bridgeId,
|
|
620
740
|
appSessionId,
|
|
621
|
-
pagePath,
|
|
741
|
+
pagePath: resolvedPagePath,
|
|
622
742
|
query: opts.query ?? {},
|
|
623
743
|
isRoot: true,
|
|
624
744
|
isTab,
|
|
@@ -699,16 +819,19 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
699
819
|
};
|
|
700
820
|
appSession.onServiceBoot = bootOnServiceLoad;
|
|
701
821
|
serviceWindow.webContents.on('did-finish-load', bootOnServiceLoad);
|
|
702
|
-
|
|
822
|
+
const spawnUrl = buildServiceHostSpawnUrl({
|
|
703
823
|
bridgeId,
|
|
704
824
|
appId,
|
|
705
|
-
pagePath,
|
|
825
|
+
pagePath: resolvedPagePath,
|
|
706
826
|
pkgRoot,
|
|
707
827
|
root,
|
|
708
828
|
resourceBaseUrl,
|
|
709
829
|
hostEnvSnapshot: hostEnv,
|
|
710
830
|
apiNamespaces,
|
|
711
|
-
})
|
|
831
|
+
});
|
|
832
|
+
void navigateServiceHost(serviceWindow, spawnUrl, {
|
|
833
|
+
onLoadFailed: err => reportServiceHostNavigationFailed(spawnUrl, err),
|
|
834
|
+
});
|
|
712
835
|
}
|
|
713
836
|
else {
|
|
714
837
|
// Fresh window: its only navigation is service.html (issued inside
|
|
@@ -717,10 +840,33 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
717
840
|
void bootServiceHost(state, appSession, ctx);
|
|
718
841
|
});
|
|
719
842
|
}
|
|
843
|
+
// Service-host crash (renderer process gone): a session-scoped hook on the
|
|
844
|
+
// service webContents, bound via the listener bag so `disposeAppSession`
|
|
845
|
+
// detaches it (matching the 'closed'/'destroyed' hooks registered above).
|
|
846
|
+
// Not a `.once` — a pooled service window can recover and crash again
|
|
847
|
+
// across its lifetime, and each crash is independently worth surfacing.
|
|
848
|
+
// Fires whether the crash lands before or after 'running'; only the launch
|
|
849
|
+
// timer needs clearing (it may already be null) since a running session no
|
|
850
|
+
// longer has one armed.
|
|
851
|
+
const onServiceCrashed = () => {
|
|
852
|
+
clearLaunchTimer(appSession);
|
|
853
|
+
ctx.diagnostics?.report({
|
|
854
|
+
severity: 'error',
|
|
855
|
+
code: 'service-host-crashed',
|
|
856
|
+
message: `Service host renderer process gone for appSessionId=${appSessionId}`,
|
|
857
|
+
appSessionId,
|
|
858
|
+
});
|
|
859
|
+
pushRuntimeStatus(ctx, appSession, { phase: 'crashed', code: 'service-host-crashed' });
|
|
860
|
+
};
|
|
861
|
+
appSession.listenerBag.on(serviceWindow.webContents, 'render-process-gone', onServiceCrashed);
|
|
862
|
+
pushRuntimeStatus(ctx, appSession, { phase: 'launching' });
|
|
863
|
+
startLaunchTimer(state, ctx, appSession);
|
|
720
864
|
return {
|
|
721
865
|
appSessionId,
|
|
722
866
|
bridgeId,
|
|
723
867
|
pagePath,
|
|
868
|
+
resolvedPagePath,
|
|
869
|
+
pageFallbackApplied,
|
|
724
870
|
serviceWcId: serviceWindow.webContents.id,
|
|
725
871
|
resourceBaseUrl,
|
|
726
872
|
manifest,
|
|
@@ -736,6 +882,15 @@ async function handlePageOpen(state, event, opts) {
|
|
|
736
882
|
throw new Error('[bridge-router] PAGE_OPEN rejected: caller not bound to app session');
|
|
737
883
|
}
|
|
738
884
|
const pagePath = normalizePagePath(opts.pagePath);
|
|
885
|
+
// Defense-in-depth against a page absent from the compiled manifest: the
|
|
886
|
+
// nav gate (handleNavActionApi) already blocks normal navigateTo/switchTab
|
|
887
|
+
// traffic upstream, but PAGE_OPEN is reachable directly (any IPC caller), so
|
|
888
|
+
// this refuses to register a PageSession main can never load resources for.
|
|
889
|
+
// Only enforced against a real compiled manifest ('app-config') — a
|
|
890
|
+
// 'fallback' manifest has no compiled truth to validate membership against.
|
|
891
|
+
if (ap.manifest.source === 'app-config' && !ap.manifest.pages.includes(pagePath)) {
|
|
892
|
+
throw new Error(`[bridge-router] PAGE_OPEN rejected: page-not-found "${pagePath}" is not in the compiled manifest`);
|
|
893
|
+
}
|
|
739
894
|
const bridgeId = opts.bridgeId || newBridgeId();
|
|
740
895
|
const windowConfig = resolvePageWindowConfig(ap.appConfig, pagePath);
|
|
741
896
|
const isTab = isTabPage(ap.appConfig, pagePath);
|
|
@@ -808,6 +963,10 @@ async function bootServiceHost(state, ap, ctx) {
|
|
|
808
963
|
// app's logic.js into the next spawn (the recycled webContents is shared).
|
|
809
964
|
if (state.appSessions.get(ap.appSessionId) !== ap)
|
|
810
965
|
return;
|
|
966
|
+
// The service wc just did-finish-load'd (that's how this got invoked) — flush
|
|
967
|
+
// any diagnostic queued for this session (or the global bucket) into its now
|
|
968
|
+
// resolvable console. Safe to call even when nothing is queued.
|
|
969
|
+
ctx.consoleForwarder?.notifyServiceHostReady?.(ap.appSessionId);
|
|
811
970
|
ap.logicInjected = await injectLogicBundle(ap);
|
|
812
971
|
if (!ap.logicInjected) {
|
|
813
972
|
// The compiled logic.js never executed, so `modDefine` registered nothing.
|
|
@@ -816,7 +975,9 @@ async function bootServiceHost(state, ap, ctx) {
|
|
|
816
975
|
// cause (unreachable resource tree / failed compile / unresolved appId).
|
|
817
976
|
// Skip it and surface one actionable diagnostic instead; the render side
|
|
818
977
|
// gates on the same flag in `routeFromRender`.
|
|
819
|
-
reportLogicLoadFailure(ap, ctx);
|
|
978
|
+
const reason = reportLogicLoadFailure(ap, ctx);
|
|
979
|
+
clearLaunchTimer(ap);
|
|
980
|
+
pushRuntimeStatus(ctx, ap, { phase: 'launch-failed', code: 'logic-bundle-unreachable', reason });
|
|
820
981
|
return;
|
|
821
982
|
}
|
|
822
983
|
// A root page absent from the compiled manifest — most commonly a page the
|
|
@@ -832,7 +993,7 @@ async function bootServiceHost(state, ap, ctx) {
|
|
|
832
993
|
const rootPage = ap.pages.get(ap.appSessionId);
|
|
833
994
|
const rootMissing = !!rootPage && !pageInManifest(ap, rootPage.pagePath);
|
|
834
995
|
if (rootMissing) {
|
|
835
|
-
reportPageNotFound(
|
|
996
|
+
reportPageNotFound(ctx, ap.appSessionId, rootPage.pagePath);
|
|
836
997
|
}
|
|
837
998
|
else if (rootPage) {
|
|
838
999
|
// serviceLoaded is flipped only when service responds with
|
|
@@ -894,34 +1055,65 @@ async function injectLogicBundle(ap) {
|
|
|
894
1055
|
/**
|
|
895
1056
|
* Emit one actionable diagnostic when the compiled logic bundle could not be
|
|
896
1057
|
* loaded — the single point where "the app's compiled resource tree is
|
|
897
|
-
* unreachable" is known with certainty.
|
|
898
|
-
*
|
|
1058
|
+
* unreachable" is known with certainty. Reported through `ctx.diagnostics`,
|
|
1059
|
+
* which mirrors it to the main-process log AND — via `consoleForwarder`'s
|
|
1060
|
+
* subscription — injects it into the service host's own console, so it
|
|
1061
|
+
* actually surfaces in the embedded DevTools Console panel where the
|
|
899
1062
|
* developer would otherwise see only the misleading `module app not found`.
|
|
1063
|
+
* `guestConsole.emit` is kept alongside for the automation `App.logAdded`
|
|
1064
|
+
* subscriber, which does not consume the diagnostics bus.
|
|
1065
|
+
*
|
|
1066
|
+
* Returns the short (pre-hint) form of the message so `bootServiceHost` can
|
|
1067
|
+
* reuse it verbatim as the `sessionRuntimeStatus` `reason` instead of
|
|
1068
|
+
* duplicating the wording.
|
|
900
1069
|
*/
|
|
901
1070
|
function reportLogicLoadFailure(ap, ctx) {
|
|
902
1071
|
const hint = ap.appId === 'unknown'
|
|
903
1072
|
? ' appId could not be resolved (it fell back to "unknown") — the mini-program likely failed to compile or its project manifest/app config is missing.'
|
|
904
1073
|
: '';
|
|
905
|
-
const
|
|
1074
|
+
const shortReason = `[dimina-kit] Failed to load the mini-program logic bundle from ${logicBundleUrl(ap)}.`;
|
|
1075
|
+
const message = `${shortReason} `
|
|
906
1076
|
+ 'The service runtime has no registered modules, so no page can mount.'
|
|
907
1077
|
+ hint
|
|
908
1078
|
+ ` Verify the project compiled successfully and that the resource server serves "${ap.appId}/${ap.root}/".`;
|
|
909
|
-
|
|
1079
|
+
ctx.diagnostics?.report({
|
|
1080
|
+
severity: 'error',
|
|
1081
|
+
code: 'logic-bundle-unreachable',
|
|
1082
|
+
message,
|
|
1083
|
+
appSessionId: ap.appSessionId,
|
|
1084
|
+
});
|
|
910
1085
|
ctx.guestConsole?.emit({ source: 'service', level: 'error', args: [message] });
|
|
1086
|
+
return shortReason;
|
|
911
1087
|
}
|
|
912
1088
|
/** Whether `pagePath` still exists in the app's freshly compiled manifest. */
|
|
913
1089
|
function pageInManifest(ap, pagePath) {
|
|
1090
|
+
// A 'fallback' manifest (app-config.json unreachable) holds only the spawn
|
|
1091
|
+
// request — no compiled truth to check membership against. Every mount gate
|
|
1092
|
+
// keyed on manifest membership must agree: the nav/PAGE_OPEN front gates
|
|
1093
|
+
// already let a fallback manifest through, so this back gate (render
|
|
1094
|
+
// loadResource) must too, or a page they admitted silently never loads.
|
|
1095
|
+
if (ap.manifest.source !== 'app-config')
|
|
1096
|
+
return true;
|
|
914
1097
|
return ap.manifest.pages.includes(normalizePagePath(pagePath));
|
|
915
1098
|
}
|
|
916
1099
|
/**
|
|
917
|
-
* WeChat-devtools-style "page does not exist" diagnostic.
|
|
918
|
-
*
|
|
919
|
-
*
|
|
1100
|
+
* WeChat-devtools-style "page does not exist" diagnostic. Reported through
|
|
1101
|
+
* `ctx.diagnostics` (main log + injected into the embedded Console panel via
|
|
1102
|
+
* `consoleForwarder`) when a mount targets a pagePath that is not in the
|
|
1103
|
+
* compiled manifest — most commonly a page the developer deleted, then
|
|
1104
|
+
* hot-reloaded. `guestConsole.emit` is kept alongside for the automation
|
|
1105
|
+
* subscriber.
|
|
920
1106
|
*/
|
|
921
|
-
function reportPageNotFound(
|
|
922
|
-
const
|
|
1107
|
+
function reportPageNotFound(ctx, appSessionId, pagePath, fallbackTo) {
|
|
1108
|
+
const base = `Page[${pagePath}] not found. May be caused by: 1. Forgetting to add page route in app.json. `
|
|
923
1109
|
+ '2. Invoking Page() in async task.';
|
|
924
|
-
|
|
1110
|
+
const message = fallbackTo ? `${base} Falling back to "${fallbackTo}".` : base;
|
|
1111
|
+
ctx.diagnostics?.report({
|
|
1112
|
+
severity: 'error',
|
|
1113
|
+
code: 'page-not-found',
|
|
1114
|
+
message,
|
|
1115
|
+
appSessionId,
|
|
1116
|
+
});
|
|
925
1117
|
ctx.guestConsole?.emit({ source: 'service', level: 'error', args: [message] });
|
|
926
1118
|
}
|
|
927
1119
|
function maybeSendResourceLoaded(ap, page) {
|
|
@@ -1000,6 +1192,18 @@ function routeFromRender(state, ap, page, msg, ctx) {
|
|
|
1000
1192
|
handleContainerMsg(ap, page, msg, ctx, state);
|
|
1001
1193
|
}
|
|
1002
1194
|
}
|
|
1195
|
+
/**
|
|
1196
|
+
* Report 'service-uncaught-error' for a consoleLog(source:'service') body —
|
|
1197
|
+
* the service preload's uncaught error/unhandledrejection post. CDP's
|
|
1198
|
+
* `Runtime.consoleAPICalled` (services/service-console) never observes these
|
|
1199
|
+
* (they never call `console.*`), so this diagnostics report is the only way
|
|
1200
|
+
* one reaches the Console panel / main log.
|
|
1201
|
+
*/
|
|
1202
|
+
function reportServiceUncaughtError(ctx, ap, body) {
|
|
1203
|
+
const severity = body.level === 'error' ? 'error' : body.level === 'warn' ? 'warn' : 'info';
|
|
1204
|
+
const message = Array.isArray(body.args) ? body.args.map(a => String(a)).join(' ') : String(body.args ?? '');
|
|
1205
|
+
ctx.diagnostics?.report({ severity, code: 'service-uncaught-error', message, appSessionId: ap.appSessionId });
|
|
1206
|
+
}
|
|
1003
1207
|
function handleContainerMsg(ap, page, msg, ctx, state) {
|
|
1004
1208
|
switch (msg.type) {
|
|
1005
1209
|
case 'serviceResourceLoaded':
|
|
@@ -1019,12 +1223,12 @@ function handleContainerMsg(ap, page, msg, ctx, state) {
|
|
|
1019
1223
|
ap.simulatorWc.send(E.DOM_READY, { bridgeId: page.bridgeId });
|
|
1020
1224
|
}
|
|
1021
1225
|
state.emitRenderEvent({ kind: 'domReady', appId: ap.appId, bridgeId: page.bridgeId });
|
|
1226
|
+
markSessionRunning(ctx, ap, page);
|
|
1022
1227
|
break;
|
|
1023
1228
|
case 'invokeAPI':
|
|
1024
1229
|
void handleSimulatorApi(state, ap, page, msg.body, ctx);
|
|
1025
1230
|
break;
|
|
1026
1231
|
case 'serviceHostError': {
|
|
1027
|
-
console.warn('[bridge-router] service host error:', msg.body);
|
|
1028
1232
|
// Forward the error to every registered `wx.onError` listener. (The dimina
|
|
1029
1233
|
// service runtime's App lifecycle is only onLaunch/onShow/onHide, so it
|
|
1030
1234
|
// does not dispatch `App.onError` — `wx.onError` is the supported path.)
|
|
@@ -1032,19 +1236,32 @@ function handleContainerMsg(ap, page, msg, ctx, state) {
|
|
|
1032
1236
|
// during lifecycle/event dispatch here.
|
|
1033
1237
|
const errBody = msg.body;
|
|
1034
1238
|
const errArg = errBody?.message ?? msg.body;
|
|
1239
|
+
// Reported through the diagnostics bus (main log + Console-panel
|
|
1240
|
+
// injection); `ctx.diagnostics.report` replaces the bare `console.warn`
|
|
1241
|
+
// this case used to make directly.
|
|
1242
|
+
ctx.diagnostics?.report({
|
|
1243
|
+
severity: 'error',
|
|
1244
|
+
code: 'service-host-error',
|
|
1245
|
+
message: String(errArg),
|
|
1246
|
+
appSessionId: ap.appSessionId,
|
|
1247
|
+
});
|
|
1035
1248
|
for (const id of state.appLifecycle.listeners(ap.appSessionId, 'onError')) {
|
|
1036
1249
|
sendCallback(ap, id, errArg);
|
|
1037
1250
|
}
|
|
1038
1251
|
break;
|
|
1039
1252
|
}
|
|
1040
|
-
case 'consoleLog':
|
|
1253
|
+
case 'consoleLog': {
|
|
1041
1254
|
// Native-host console capture: the render-host / service-host guest preloads
|
|
1042
1255
|
// monkeypatch console.* and post each entry here (one case handles both —
|
|
1043
1256
|
// distinguish via msg.body.source). Forward to the console sink (set by the
|
|
1044
1257
|
// automation server under native-host) so it can rebroadcast as App.logAdded.
|
|
1045
1258
|
// Guarded + non-throwing: console capture must never break message routing.
|
|
1046
1259
|
ctx.guestConsole?.emit(msg.body);
|
|
1260
|
+
const body = msg.body;
|
|
1261
|
+
if (body?.source === 'service')
|
|
1262
|
+
reportServiceUncaughtError(ctx, ap, body);
|
|
1047
1263
|
break;
|
|
1264
|
+
}
|
|
1048
1265
|
case 'storageChanged':
|
|
1049
1266
|
// Native-host SYNC storage liveness: the service-host's `setStorageSync`/etc.
|
|
1050
1267
|
// write `localStorage` directly (no main round-trip), so they post the change
|
|
@@ -1136,86 +1353,160 @@ const APP_LIFECYCLE_UNREGISTER = {
|
|
|
1136
1353
|
offAppHide: 'onAppHide',
|
|
1137
1354
|
offError: 'onError',
|
|
1138
1355
|
};
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
if (NAV_BAR_API_NAMES.has(name)) {
|
|
1143
|
-
if (!ap.simulatorWc.isDestroyed()) {
|
|
1144
|
-
ap.simulatorWc.send(E.NAV_BAR, {
|
|
1145
|
-
bridgeId: page.bridgeId,
|
|
1146
|
-
name,
|
|
1147
|
-
params,
|
|
1148
|
-
});
|
|
1149
|
-
}
|
|
1150
|
-
const successResult = { errMsg: `${name}:ok` };
|
|
1151
|
-
sendCallback(ap, params.success, successResult);
|
|
1152
|
-
sendCallback(ap, params.complete, successResult);
|
|
1153
|
-
return;
|
|
1154
|
-
}
|
|
1155
|
-
if (NAV_ACTION_NAMES.has(name)) {
|
|
1156
|
-
const payload = {
|
|
1157
|
-
appSessionId: ap.appSessionId,
|
|
1356
|
+
function handleNavBarApi(ap, page, name, params) {
|
|
1357
|
+
if (!ap.simulatorWc.isDestroyed()) {
|
|
1358
|
+
ap.simulatorWc.send(E.NAV_BAR, {
|
|
1158
1359
|
bridgeId: page.bridgeId,
|
|
1159
|
-
name
|
|
1360
|
+
name,
|
|
1160
1361
|
params,
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
const successResult = { errMsg: `${name}:ok` };
|
|
1365
|
+
sendCallback(ap, params.success, successResult);
|
|
1366
|
+
sendCallback(ap, params.complete, successResult);
|
|
1367
|
+
}
|
|
1368
|
+
// Fails a nav/tab action's service-side callback through the same success/
|
|
1369
|
+
// complete dispatch every other API branch uses — the one mechanism for
|
|
1370
|
+
// answering a call that never reaches (or never should reach) the simulator.
|
|
1371
|
+
function failActionCallback(ap, params, errMsg) {
|
|
1372
|
+
const fail = { errMsg };
|
|
1373
|
+
sendCallback(ap, params.fail, fail);
|
|
1374
|
+
sendCallback(ap, params.complete, fail);
|
|
1375
|
+
}
|
|
1376
|
+
// Shared by handleNavActionApi/handleTabActionApi: both forward a same-shaped
|
|
1377
|
+
// payload to the simulator window, differing only in the event name and the
|
|
1378
|
+
// payload's static `name` union. Simulator-destroyed fails the callback
|
|
1379
|
+
// directly since there is no ack path to fail through otherwise.
|
|
1380
|
+
function sendActionOrFail(ap, eventName, payload, name, params) {
|
|
1381
|
+
if (!ap.simulatorWc.isDestroyed()) {
|
|
1382
|
+
ap.simulatorWc.send(eventName, payload);
|
|
1171
1383
|
return;
|
|
1172
1384
|
}
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1385
|
+
failActionCallback(ap, params, `${name}:fail simulator window destroyed`);
|
|
1386
|
+
}
|
|
1387
|
+
/** The bare page path a nav/tab action targets (query-stripped, normalized), or undefined for an action carrying none (navigateBack). */
|
|
1388
|
+
function extractNavTargetPagePath(params) {
|
|
1389
|
+
const raw = params.url;
|
|
1390
|
+
if (typeof raw !== 'string' || !raw)
|
|
1391
|
+
return undefined;
|
|
1392
|
+
return normalizePagePath(raw.split('?')[0] ?? raw);
|
|
1393
|
+
}
|
|
1394
|
+
/**
|
|
1395
|
+
* Whether a nav/tab action's target page is mountable — checked ONLY against
|
|
1396
|
+
* a real compiled manifest (`source === 'app-config'`); a `'fallback'`
|
|
1397
|
+
* manifest can't tell mountable from not, so it lets everything through.
|
|
1398
|
+
*/
|
|
1399
|
+
function checkNavTarget(ap, name, targetPagePath) {
|
|
1400
|
+
if (ap.manifest.source !== 'app-config')
|
|
1401
|
+
return { ok: true };
|
|
1402
|
+
if (!ap.manifest.pages.includes(targetPagePath)) {
|
|
1403
|
+
return { ok: false, errMsg: `${name}:fail page "${targetPagePath}" is not found`, reportNotFound: true };
|
|
1404
|
+
}
|
|
1405
|
+
if (name === 'switchTab') {
|
|
1406
|
+
const inTabBar = ap.manifest.tabBar?.list.some(item => normalizePagePath(item.pagePath) === targetPagePath) ?? false;
|
|
1407
|
+
if (!inTabBar)
|
|
1408
|
+
return { ok: false, errMsg: 'switchTab:fail can not switch to no-tabBar page' };
|
|
1409
|
+
}
|
|
1410
|
+
return { ok: true };
|
|
1411
|
+
}
|
|
1412
|
+
function handleNavActionApi(ap, ctx, page, name, params) {
|
|
1413
|
+
const targetPagePath = extractNavTargetPagePath(params);
|
|
1414
|
+
if (targetPagePath !== undefined) {
|
|
1415
|
+
const verdict = checkNavTarget(ap, name, targetPagePath);
|
|
1416
|
+
if (!verdict.ok) {
|
|
1417
|
+
if (verdict.reportNotFound)
|
|
1418
|
+
reportPageNotFound(ctx, ap.appSessionId, targetPagePath);
|
|
1419
|
+
failActionCallback(ap, params, verdict.errMsg);
|
|
1420
|
+
return;
|
|
1188
1421
|
}
|
|
1189
|
-
return;
|
|
1190
1422
|
}
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1423
|
+
const payload = {
|
|
1424
|
+
appSessionId: ap.appSessionId,
|
|
1425
|
+
bridgeId: page.bridgeId,
|
|
1426
|
+
name: name,
|
|
1427
|
+
params,
|
|
1428
|
+
callbacks: extractCallbacks(params),
|
|
1429
|
+
};
|
|
1430
|
+
sendActionOrFail(ap, E.NAV_ACTION, payload, name, params);
|
|
1431
|
+
}
|
|
1432
|
+
function handleTabActionApi(ap, page, name, params) {
|
|
1433
|
+
const payload = {
|
|
1434
|
+
appSessionId: ap.appSessionId,
|
|
1435
|
+
bridgeId: page.bridgeId,
|
|
1436
|
+
name: name,
|
|
1437
|
+
params,
|
|
1438
|
+
callbacks: extractCallbacks(params),
|
|
1439
|
+
};
|
|
1440
|
+
sendActionOrFail(ap, E.TAB_ACTION, payload, name, params);
|
|
1441
|
+
}
|
|
1442
|
+
// App-level lifecycle listeners (wx.onAppShow / onAppHide / onError + off*).
|
|
1443
|
+
// The service encodes the listener as a keep callback id in `params.success`;
|
|
1444
|
+
// we store it and re-fire on main-window foreground/background (the driver in
|
|
1445
|
+
// installBridgeRouter) and on serviceHostError. `off*` clears the event's
|
|
1446
|
+
// listeners for the session. Returns true when `name` matched a lifecycle
|
|
1447
|
+
// register/unregister call (fully handled), false to fall through.
|
|
1448
|
+
function handleAppLifecycleToggle(state, ap, name, params) {
|
|
1196
1449
|
const lifecycleRegister = APP_LIFECYCLE_REGISTER[name];
|
|
1197
1450
|
if (lifecycleRegister) {
|
|
1198
1451
|
state.appLifecycle.register(ap.appSessionId, lifecycleRegister, params.success);
|
|
1199
|
-
return;
|
|
1452
|
+
return true;
|
|
1200
1453
|
}
|
|
1201
1454
|
const lifecycleUnregister = APP_LIFECYCLE_UNREGISTER[name];
|
|
1202
1455
|
if (lifecycleUnregister) {
|
|
1203
1456
|
// off(cb) carries the same evtId as the original on(cb) → removes just that
|
|
1204
1457
|
// listener; off() with no callback carries no id → clears the whole event.
|
|
1205
1458
|
state.appLifecycle.unregister(ap.appSessionId, lifecycleUnregister, params.success);
|
|
1459
|
+
return true;
|
|
1460
|
+
}
|
|
1461
|
+
return false;
|
|
1462
|
+
}
|
|
1463
|
+
// pageScrollTo acts on the page's render guest (scroll its document), which
|
|
1464
|
+
// only the main process can reach — run the scroll script in the invoking
|
|
1465
|
+
// page's render webContents rather than forwarding to the simulator.
|
|
1466
|
+
function handlePageScrollApi(ap, page, params) {
|
|
1467
|
+
const renderWc = page.renderWc;
|
|
1468
|
+
if (renderWc && !renderWc.isDestroyed()) {
|
|
1469
|
+
void renderWc.executeJavaScript(buildPageScrollScript(params)).catch(() => { });
|
|
1470
|
+
}
|
|
1471
|
+
const successResult = { errMsg: 'pageScrollTo:ok' };
|
|
1472
|
+
sendCallback(ap, params.success, successResult);
|
|
1473
|
+
sendCallback(ap, params.complete, successResult);
|
|
1474
|
+
}
|
|
1475
|
+
// Shared try/invoke/callback pattern used by the storageApi and
|
|
1476
|
+
// ctx.simulatorApis branches: relay the resolved value through
|
|
1477
|
+
// success+complete, or the thrown error through fail+complete with a
|
|
1478
|
+
// namespaced errMsg.
|
|
1479
|
+
async function invokeSimulatorApiAndCallback(ap, name, params, invoke) {
|
|
1480
|
+
try {
|
|
1481
|
+
const result = await invoke();
|
|
1482
|
+
sendCallback(ap, params.success, result);
|
|
1483
|
+
sendCallback(ap, params.complete, result);
|
|
1484
|
+
}
|
|
1485
|
+
catch (error) {
|
|
1486
|
+
const failResult = { errMsg: `${name}:fail ${error instanceof Error ? error.message : String(error)}` };
|
|
1487
|
+
sendCallback(ap, params.fail, failResult);
|
|
1488
|
+
sendCallback(ap, params.complete, failResult);
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
async function handleSimulatorApi(state, ap, page, body, ctx) {
|
|
1492
|
+
const name = String(body.name ?? '');
|
|
1493
|
+
const params = normalizeParams(body.params);
|
|
1494
|
+
if (NAV_BAR_API_NAMES.has(name)) {
|
|
1495
|
+
handleNavBarApi(ap, page, name, params);
|
|
1496
|
+
return;
|
|
1497
|
+
}
|
|
1498
|
+
if (NAV_ACTION_NAMES.has(name)) {
|
|
1499
|
+
handleNavActionApi(ap, ctx, page, name, params);
|
|
1500
|
+
return;
|
|
1501
|
+
}
|
|
1502
|
+
if (TAB_ACTION_NAMES.has(name)) {
|
|
1503
|
+
handleTabActionApi(ap, page, name, params);
|
|
1206
1504
|
return;
|
|
1207
1505
|
}
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
// page's render webContents rather than forwarding to the simulator.
|
|
1506
|
+
if (handleAppLifecycleToggle(state, ap, name, params))
|
|
1507
|
+
return;
|
|
1211
1508
|
if (name === 'pageScrollTo') {
|
|
1212
|
-
|
|
1213
|
-
if (renderWc && !renderWc.isDestroyed()) {
|
|
1214
|
-
void renderWc.executeJavaScript(buildPageScrollScript(params)).catch(() => { });
|
|
1215
|
-
}
|
|
1216
|
-
const successResult = { errMsg: 'pageScrollTo:ok' };
|
|
1217
|
-
sendCallback(ap, params.success, successResult);
|
|
1218
|
-
sendCallback(ap, params.complete, successResult);
|
|
1509
|
+
handlePageScrollApi(ap, page, params);
|
|
1219
1510
|
return;
|
|
1220
1511
|
}
|
|
1221
1512
|
// Native-host storage unification: route async wx.setStorage/getStorage/etc.
|
|
@@ -1224,16 +1515,8 @@ async function handleSimulatorApi(state, ap, page, body, ctx) {
|
|
|
1224
1515
|
// http:// origin. Without this, async writes and sync writes diverge across
|
|
1225
1516
|
// two origins even for the running mini-app.
|
|
1226
1517
|
if (ctx.storageApi && STORAGE_API_NAMES.has(name)) {
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
sendCallback(ap, params.success, result);
|
|
1230
|
-
sendCallback(ap, params.complete, result);
|
|
1231
|
-
}
|
|
1232
|
-
catch (error) {
|
|
1233
|
-
const failResult = { errMsg: `${name}:fail ${error instanceof Error ? error.message : String(error)}` };
|
|
1234
|
-
sendCallback(ap, params.fail, failResult);
|
|
1235
|
-
sendCallback(ap, params.complete, failResult);
|
|
1236
|
-
}
|
|
1518
|
+
const storageApi = ctx.storageApi;
|
|
1519
|
+
await invokeSimulatorApiAndCallback(ap, name, params, () => storageApi.invoke(ap.appId, name, params));
|
|
1237
1520
|
return;
|
|
1238
1521
|
}
|
|
1239
1522
|
// Main-process registry (downstream-registered host APIs via
|
|
@@ -1242,16 +1525,7 @@ async function handleSimulatorApi(state, ap, page, body, ctx) {
|
|
|
1242
1525
|
// MiniApp owns the DOM-touching defaults (wx.getSystemInfo, chooseImage,
|
|
1243
1526
|
// chooseMedia, fs.*, …) and the bridge-router can't run those itself.
|
|
1244
1527
|
if (ctx.simulatorApis.has(name)) {
|
|
1245
|
-
|
|
1246
|
-
const result = await ctx.simulatorApis.invoke(name, params);
|
|
1247
|
-
sendCallback(ap, params.success, result);
|
|
1248
|
-
sendCallback(ap, params.complete, result);
|
|
1249
|
-
}
|
|
1250
|
-
catch (error) {
|
|
1251
|
-
const failResult = { errMsg: `${name}:fail ${error instanceof Error ? error.message : String(error)}` };
|
|
1252
|
-
sendCallback(ap, params.fail, failResult);
|
|
1253
|
-
sendCallback(ap, params.complete, failResult);
|
|
1254
|
-
}
|
|
1528
|
+
await invokeSimulatorApiAndCallback(ap, name, params, () => ctx.simulatorApis.invoke(name, params));
|
|
1255
1529
|
return;
|
|
1256
1530
|
}
|
|
1257
1531
|
forwardApiCallToSimulator(state, ap, page, name, params);
|
|
@@ -1267,12 +1541,17 @@ function forwardApiCallToSimulator(state, ap, page, name, params) {
|
|
|
1267
1541
|
const requestId = newRequestId();
|
|
1268
1542
|
// Persistent subscriptions (`keep: true`, e.g. audioListen) bind their event
|
|
1269
1543
|
// bridge synchronously but only emit their FIRST response when the first
|
|
1270
|
-
// event fires (which can be well past the
|
|
1544
|
+
// event fires (which can be well past the no-handler window). Arming the
|
|
1271
1545
|
// one-shot timeout would tear the subscription down before it ever delivers,
|
|
1272
1546
|
// so keep calls run without a timeout and are reaped on page/app teardown.
|
|
1273
1547
|
// Recognise persistent subscriptions BY NAME (`audioListen`) too: the service
|
|
1274
1548
|
// host strips the original `keep: true` before forwarding, so `params.keep` is
|
|
1275
1549
|
// gone by the time we route the call. See shared/simulator-api-metadata.ts.
|
|
1550
|
+
//
|
|
1551
|
+
// The watchdog window is per-call (apiCallWatchdogMs): network-budget APIs
|
|
1552
|
+
// (request/downloadFile/uploadFile) get their wx timeout budget + grace so a
|
|
1553
|
+
// slow-but-alive HTTP call is never torn down before its handler's own
|
|
1554
|
+
// deadline; everything else keeps the flat 5s missing-handler window.
|
|
1276
1555
|
const keep = params.keep === true || isPersistentSimulatorApi(name);
|
|
1277
1556
|
const timer = keep
|
|
1278
1557
|
? undefined
|
|
@@ -1287,7 +1566,7 @@ function forwardApiCallToSimulator(state, ap, page, name, params) {
|
|
|
1287
1566
|
const fail = { errMsg: `${pending.name}:fail no handler (timeout)` };
|
|
1288
1567
|
sendCallback(target, pending.callbacks.fail, fail);
|
|
1289
1568
|
sendCallback(target, pending.callbacks.complete, fail);
|
|
1290
|
-
},
|
|
1569
|
+
}, apiCallWatchdogMs(name, params));
|
|
1291
1570
|
state.pendingApiCalls.set(requestId, {
|
|
1292
1571
|
appSessionId: ap.appSessionId,
|
|
1293
1572
|
callbacks,
|
|
@@ -1360,7 +1639,20 @@ function handleApiResponse(state, sender, payload) {
|
|
|
1360
1639
|
sendCallback(ap, pending.callbacks.complete, payload.result ?? { errMsg: `${pending.name}:ok` });
|
|
1361
1640
|
}
|
|
1362
1641
|
else {
|
|
1363
|
-
|
|
1642
|
+
// The router is a transport, not an author: the simulator-side fail result
|
|
1643
|
+
// (errno etc.) must reach the service callbacks intact, so spread it
|
|
1644
|
+
// through instead of rebuilding a bare { errMsg }. errMsg is guaranteed
|
|
1645
|
+
// non-empty — `||` (not `??`) so an empty string (e.g. an HTTP/2 response
|
|
1646
|
+
// whose statusText is always '') falls through payload.errMsg →
|
|
1647
|
+
// result.errMsg → the `${name}:fail` default instead of surfacing blank.
|
|
1648
|
+
const result = payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result)
|
|
1649
|
+
? payload.result
|
|
1650
|
+
: undefined;
|
|
1651
|
+
const resultErrMsg = typeof result?.errMsg === 'string' ? result.errMsg : undefined;
|
|
1652
|
+
const fail = {
|
|
1653
|
+
...result,
|
|
1654
|
+
errMsg: payload.errMsg || resultErrMsg || `${pending.name}:fail`,
|
|
1655
|
+
};
|
|
1364
1656
|
sendCallback(ap, pending.callbacks.fail, fail);
|
|
1365
1657
|
sendCallback(ap, pending.callbacks.complete, fail);
|
|
1366
1658
|
}
|
|
@@ -1544,34 +1836,22 @@ function disposePageSession(state, ap, page) {
|
|
|
1544
1836
|
ap.pages.delete(page.bridgeId);
|
|
1545
1837
|
state.pageSessions.delete(page.bridgeId);
|
|
1546
1838
|
}
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
// Remove this session's shutdown-fallback registry entry so the registry does
|
|
1553
|
-
// not accumulate one stale closure per respawn. Ordered AFTER the delete
|
|
1554
|
-
// above: dispose() runs the wrapped fn (disposeAppSession again), which
|
|
1555
|
-
// early-returns now that the session is gone — no recursion, no double-run.
|
|
1556
|
-
// (At whole-app shutdown the registry marks the entry released before calling
|
|
1557
|
-
// the fn, so this dispose() is a no-op then.)
|
|
1558
|
-
const registryHandle = ap.registryHandle;
|
|
1559
|
-
ap.registryHandle = null;
|
|
1560
|
-
void registryHandle?.dispose();
|
|
1561
|
-
state.appLifecycle.dispose(appSessionId);
|
|
1562
|
-
// Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
|
|
1563
|
-
// page teardown below progressively empties (and finally clears).
|
|
1564
|
-
state.evictAppDataBridges(ap);
|
|
1565
|
-
// Drain any pending API calls owned by this app session. One-shot calls
|
|
1566
|
-
// normally self-clean on response/timeout, but persistent (`keep: true`)
|
|
1567
|
-
// subscriptions (e.g. audioListen) live with their timer cleared until
|
|
1568
|
-
// teardown — without this they would leak in pendingApiCalls forever.
|
|
1839
|
+
// Drain any pending API calls owned by this app session. One-shot calls
|
|
1840
|
+
// normally self-clean on response/timeout, but persistent (`keep: true`)
|
|
1841
|
+
// subscriptions (e.g. audioListen) live with their timer cleared until
|
|
1842
|
+
// teardown — without this they would leak in pendingApiCalls forever.
|
|
1843
|
+
function drainPendingApiCallsForSession(state, appSessionId) {
|
|
1569
1844
|
for (const [requestId, pending] of state.pendingApiCalls) {
|
|
1570
1845
|
if (pending.appSessionId !== appSessionId)
|
|
1571
1846
|
continue;
|
|
1572
1847
|
clearTimeout(pending.timer);
|
|
1573
1848
|
state.pendingApiCalls.delete(requestId);
|
|
1574
1849
|
}
|
|
1850
|
+
}
|
|
1851
|
+
// Closes every page's render guest and drops its wc/pageSessions bindings.
|
|
1852
|
+
// Caller clears `ap.pages` itself right after — this only tears each page
|
|
1853
|
+
// down, it doesn't touch the map.
|
|
1854
|
+
function closeSessionPages(state, ap) {
|
|
1575
1855
|
for (const page of ap.pages.values()) {
|
|
1576
1856
|
if (page.renderWc && !page.renderWc.isDestroyed()) {
|
|
1577
1857
|
state.wcIdToBridgeId.delete(page.renderWc.id);
|
|
@@ -1588,15 +1868,11 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1588
1868
|
}
|
|
1589
1869
|
state.pageSessions.delete(page.bridgeId);
|
|
1590
1870
|
}
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
// simulator wc must not accumulate one dead hook per soft reload until the
|
|
1597
|
-
// MaxListeners warning fires. Idempotent with teardown being triggered BY
|
|
1598
|
-
// one of these hooks (removing a fired once() is a no-op).
|
|
1599
|
-
ap.listenerBag.dispose();
|
|
1871
|
+
}
|
|
1872
|
+
// Three-way release of the session's service-host window: pool release (soft
|
|
1873
|
+
// reuse), pool reclaim of an externally-closed window, or a plain close for
|
|
1874
|
+
// the unpooled path.
|
|
1875
|
+
async function releaseServiceWindow(state, ap, opts) {
|
|
1600
1876
|
if (ap.poolEntryId !== null && state.pool && !opts.serviceAlreadyClosed) {
|
|
1601
1877
|
// Soft reuse (foundation.md §4.3): the pooled service-host webContents keeps
|
|
1602
1878
|
// its wc.id but is about to run a NEW app session. Reset its Connection
|
|
@@ -1623,12 +1899,14 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1623
1899
|
// pool only auto-reclaims on render-process-gone — so without this the in-use
|
|
1624
1900
|
// entry leaks in the pool forever, permanently shrinking capacity. Reclaim
|
|
1625
1901
|
// the slot explicitly (the window is already gone; releaseDestroyed won't
|
|
1626
|
-
// touch it).
|
|
1902
|
+
// touch it).
|
|
1627
1903
|
state.pool.releaseDestroyed(ap.poolEntryId);
|
|
1628
1904
|
}
|
|
1629
1905
|
else if (!opts.serviceAlreadyClosed && !ap.serviceWindow.isDestroyed()) {
|
|
1630
1906
|
ap.serviceWindow.close();
|
|
1631
1907
|
}
|
|
1908
|
+
}
|
|
1909
|
+
function unbindSessionFromSharedMaps(state, ap, appSessionId) {
|
|
1632
1910
|
// Value-checked unbind for the service wc: this delete runs after the
|
|
1633
1911
|
// pool-release await above, so on the pool path the next spawn may have
|
|
1634
1912
|
// already re-acquired the SAME window and rebound its wc id — only remove
|
|
@@ -1645,6 +1923,42 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1645
1923
|
if (hosted.size === 0)
|
|
1646
1924
|
state.simulatorWcIdToAppSessionIds.delete(ap.simulatorWc.id);
|
|
1647
1925
|
}
|
|
1926
|
+
}
|
|
1927
|
+
async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
1928
|
+
const ap = state.appSessions.get(appSessionId);
|
|
1929
|
+
if (!ap)
|
|
1930
|
+
return;
|
|
1931
|
+
state.appSessions.delete(appSessionId);
|
|
1932
|
+
// Prevent a leaked/stale watchdog: without this, a session disposed while
|
|
1933
|
+
// still 'launching' (e.g. project close mid-launch) would fire its timer
|
|
1934
|
+
// `LAUNCH_TIMEOUT_MS` later against an already-torn-down session.
|
|
1935
|
+
clearLaunchTimer(ap);
|
|
1936
|
+
// Remove this session's shutdown-fallback registry entry so the registry does
|
|
1937
|
+
// not accumulate one stale closure per respawn. Ordered AFTER the delete
|
|
1938
|
+
// above: dispose() runs the wrapped fn (disposeAppSession again), which
|
|
1939
|
+
// early-returns now that the session is gone — no recursion, no double-run.
|
|
1940
|
+
// (At whole-app shutdown the registry marks the entry released before calling
|
|
1941
|
+
// the fn, so this dispose() is a no-op then.)
|
|
1942
|
+
const registryHandle = ap.registryHandle;
|
|
1943
|
+
ap.registryHandle = null;
|
|
1944
|
+
void registryHandle?.dispose();
|
|
1945
|
+
state.appLifecycle.dispose(appSessionId);
|
|
1946
|
+
// Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
|
|
1947
|
+
// page teardown below progressively empties (and finally clears).
|
|
1948
|
+
state.evictAppDataBridges(ap);
|
|
1949
|
+
drainPendingApiCallsForSession(state, appSessionId);
|
|
1950
|
+
closeSessionPages(state, ap);
|
|
1951
|
+
ap.pages.clear();
|
|
1952
|
+
// Detach every session-scoped hook on emitters that outlive this session —
|
|
1953
|
+
// the shared simulator WCV's 'destroyed' and the service window's 'closed' —
|
|
1954
|
+
// BEFORE the window is released/closed below: a pool-recycled window must
|
|
1955
|
+
// not re-trigger this session's teardown from a stale 'closed', and the
|
|
1956
|
+
// simulator wc must not accumulate one dead hook per soft reload until the
|
|
1957
|
+
// MaxListeners warning fires. Idempotent with teardown being triggered BY
|
|
1958
|
+
// one of these hooks (removing a fired once() is a no-op).
|
|
1959
|
+
ap.listenerBag.dispose();
|
|
1960
|
+
await releaseServiceWindow(state, ap, opts);
|
|
1961
|
+
unbindSessionFromSharedMaps(state, ap, appSessionId);
|
|
1648
1962
|
// Only the local fallback server needs closing; the dev-server base is owned
|
|
1649
1963
|
// by the workspace session, not this app session.
|
|
1650
1964
|
if (ap.resourceServer) {
|
|
@@ -1654,7 +1968,14 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1654
1968
|
}
|
|
1655
1969
|
}
|
|
1656
1970
|
// ── App-config / manifest parsing ───────────────────────────────────────────
|
|
1657
|
-
|
|
1971
|
+
/**
|
|
1972
|
+
* Fetch+parse `app-config.json`. Pure (no `ctx`, no diagnostics dependency) —
|
|
1973
|
+
* a non-2xx response or a parse/network failure reports through `onUnreachable`
|
|
1974
|
+
* (the caller routes it into `ctx.diagnostics`) and returns `{}` either way, so
|
|
1975
|
+
* the spawn flow always continues with a manifest that just falls back to a
|
|
1976
|
+
* single-page/no-tabBar shape (`buildAppManifest`'s fallback).
|
|
1977
|
+
*/
|
|
1978
|
+
async function loadAppConfig(resourceBase, onUnreachable) {
|
|
1658
1979
|
// `resourceBase` is the dir/URL that directly contains `app-config.json`:
|
|
1659
1980
|
// the dev server's `<base><appId>/<root>/` (http) or the local fallback
|
|
1660
1981
|
// server root (also http). Both are HTTP, so a single fetch path covers them.
|
|
@@ -1662,23 +1983,55 @@ async function loadAppConfig(resourceBase) {
|
|
|
1662
1983
|
try {
|
|
1663
1984
|
const res = await fetch(cfgUrl);
|
|
1664
1985
|
if (!res.ok) {
|
|
1986
|
+
const error = new Error(`app-config.json fetch ${res.status} at ${cfgUrl}`);
|
|
1665
1987
|
console.warn(`[bridge-router] no app-config.json at ${cfgUrl} (${res.status})`);
|
|
1988
|
+
onUnreachable?.({ url: cfgUrl, error });
|
|
1666
1989
|
return {};
|
|
1667
1990
|
}
|
|
1668
1991
|
return await res.json();
|
|
1669
1992
|
}
|
|
1670
1993
|
catch (error) {
|
|
1671
1994
|
console.warn('[bridge-router] failed to fetch/parse app-config.json:', error);
|
|
1995
|
+
onUnreachable?.({ url: cfgUrl, error });
|
|
1672
1996
|
return {};
|
|
1673
1997
|
}
|
|
1674
1998
|
}
|
|
1675
1999
|
function buildAppManifest(appConfig, fallbackEntry) {
|
|
1676
2000
|
const entry = appConfig.app?.entryPagePath || fallbackEntry;
|
|
1677
|
-
const
|
|
2001
|
+
const hasCompiledPages = !!appConfig.app?.pages?.length;
|
|
2002
|
+
const pages = hasCompiledPages ? appConfig.app.pages : [entry];
|
|
1678
2003
|
const tabBar = appConfig.app?.tabBar && Array.isArray(appConfig.app.tabBar.list) && appConfig.app.tabBar.list.length > 0
|
|
1679
2004
|
? appConfig.app.tabBar
|
|
1680
2005
|
: undefined;
|
|
1681
|
-
return {
|
|
2006
|
+
return {
|
|
2007
|
+
entryPagePath: normalizePagePath(entry),
|
|
2008
|
+
pages: pages.map(normalizePagePath),
|
|
2009
|
+
tabBar,
|
|
2010
|
+
// 'app-config' means `pages` above is the real compiled list mount gates
|
|
2011
|
+
// can trust; 'fallback' means app-config.json was unreachable and `pages`
|
|
2012
|
+
// is just the single requested page — nothing to validate membership
|
|
2013
|
+
// against, so every gate keyed on this source lets it through unchanged.
|
|
2014
|
+
source: hasCompiledPages ? 'app-config' : 'fallback',
|
|
2015
|
+
};
|
|
2016
|
+
}
|
|
2017
|
+
/**
|
|
2018
|
+
* Resolve the root page a spawn actually mounts against the compiled
|
|
2019
|
+
* manifest. The request is trusted verbatim unless the manifest is a real
|
|
2020
|
+
* compiled one (`source === 'app-config'`) AND the request is absent from its
|
|
2021
|
+
* `pages` — in that case the request is unmountable (most commonly a start
|
|
2022
|
+
* page removed by a hot reload) and resolution falls back to
|
|
2023
|
+
* `manifest.entryPagePath`, or `manifest.pages[0]` when even the entry isn't a
|
|
2024
|
+
* member. A `'fallback'` manifest contains only the request itself, so there
|
|
2025
|
+
* is trivially nothing to fall back from.
|
|
2026
|
+
*/
|
|
2027
|
+
function resolveRootPagePath(manifest, requestedPagePath) {
|
|
2028
|
+
if (manifest.source !== 'app-config' || manifest.pages.includes(requestedPagePath)) {
|
|
2029
|
+
return { resolvedPagePath: requestedPagePath, pageFallbackApplied: false };
|
|
2030
|
+
}
|
|
2031
|
+
const resolvedPagePath = manifest.pages.includes(manifest.entryPagePath)
|
|
2032
|
+
? manifest.entryPagePath
|
|
2033
|
+
: manifest.pages[0];
|
|
2034
|
+
return { resolvedPagePath, pageFallbackApplied: true };
|
|
1682
2035
|
}
|
|
1683
2036
|
function resolvePageWindowConfig(appConfig, pagePath) {
|
|
1684
2037
|
const normalized = normalizePagePath(pagePath);
|