@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
|
@@ -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,6 @@ 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
|
-
/** Default timeout for a simulator-forwarded API call. */
|
|
96
|
-
const API_CALL_TIMEOUT_MS = 5_000;
|
|
97
103
|
// Same-appId matches prefer the MOST RECENT spawn (Maps preserve insertion
|
|
98
104
|
// order): after a respawn/reopen the newest session is the live one — the
|
|
99
105
|
// first match could be a just-superseded session mid-teardown. The loop must
|
|
@@ -343,13 +349,28 @@ export function installBridgeRouter(ctx) {
|
|
|
343
349
|
// Chrome DevTools (attached to the service host) prefixed `[视图]` — regardless
|
|
344
350
|
// of whether an automation client is connected. Automation now SUBSCRIBES to
|
|
345
351
|
// this forwarder instead of clobbering `ctx.guestConsole`.
|
|
346
|
-
|
|
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);
|
|
347
364
|
ctx.consoleForwarder = consoleForwarder;
|
|
348
365
|
ctx.guestConsole = consoleForwarder;
|
|
349
366
|
ctx.registry.add(() => {
|
|
350
367
|
void consoleForwarder.dispose();
|
|
351
368
|
ctx.consoleForwarder = undefined;
|
|
352
369
|
ctx.guestConsole = undefined;
|
|
370
|
+
if (ownsDiagnosticsBus) {
|
|
371
|
+
diagnosticsBus.dispose();
|
|
372
|
+
ctx.diagnostics = undefined;
|
|
373
|
+
}
|
|
353
374
|
});
|
|
354
375
|
// DeviceShell → main: record the visible top-of-stack page bridgeId so the
|
|
355
376
|
// accessor above can resolve "the active page". Sender-validated against the
|
|
@@ -516,6 +537,64 @@ export function installBridgeRouter(ctx) {
|
|
|
516
537
|
});
|
|
517
538
|
}
|
|
518
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
|
+
}
|
|
519
598
|
async function handleSpawn(state, ctx, event, opts) {
|
|
520
599
|
const appId = opts.appId;
|
|
521
600
|
if (!appId)
|
|
@@ -562,10 +641,21 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
562
641
|
});
|
|
563
642
|
// app-config.json lives at `<base><appId>/<root>/app-config.json` on the dev
|
|
564
643
|
// server, or at the local server root for the fallback path.
|
|
565
|
-
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
|
+
});
|
|
566
652
|
const manifest = buildAppManifest(appConfig, pagePath);
|
|
567
|
-
const
|
|
568
|
-
|
|
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);
|
|
569
659
|
// Acquire a pre-warmed service-host window when pooling is enabled; otherwise
|
|
570
660
|
// construct one fresh (default). A pooled/fallback window is warmed on
|
|
571
661
|
// about:blank and must be navigated to the spawn URL below; the fresh path
|
|
@@ -573,13 +663,31 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
573
663
|
const usedPool = state.pool !== null;
|
|
574
664
|
let poolEntryId = null;
|
|
575
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
|
+
};
|
|
576
684
|
if (state.pool) {
|
|
577
685
|
const acquired = await state.pool.acquire(serviceHostSpec());
|
|
578
686
|
serviceWindow = acquired.win;
|
|
579
687
|
poolEntryId = acquired.entryId;
|
|
580
688
|
}
|
|
581
689
|
else {
|
|
582
|
-
|
|
690
|
+
const freshWindowOptions = {
|
|
583
691
|
bridgeId,
|
|
584
692
|
appId,
|
|
585
693
|
// Same (appId, projectPath) pair the simulator WCV uses, so this project's
|
|
@@ -589,12 +697,16 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
589
697
|
// awaits above would otherwise give the service host a different path than
|
|
590
698
|
// the simulator WCV was built with, splitting the partition.
|
|
591
699
|
projectPath: workspaceProjectPath || undefined,
|
|
592
|
-
pagePath,
|
|
700
|
+
pagePath: resolvedPagePath,
|
|
593
701
|
pkgRoot,
|
|
594
702
|
root,
|
|
595
703
|
resourceBaseUrl,
|
|
596
704
|
hostEnvSnapshot: hostEnv,
|
|
597
705
|
apiNamespaces,
|
|
706
|
+
};
|
|
707
|
+
serviceWindow = createServiceHostWindow({
|
|
708
|
+
...freshWindowOptions,
|
|
709
|
+
onLoadFailed: err => reportServiceHostNavigationFailed(buildServiceHostSpawnUrl(freshWindowOptions), err),
|
|
598
710
|
});
|
|
599
711
|
}
|
|
600
712
|
const appSession = {
|
|
@@ -619,11 +731,14 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
619
731
|
onServiceBoot: null,
|
|
620
732
|
listenerBag: createSessionListenerBag(),
|
|
621
733
|
registryHandle: null,
|
|
734
|
+
running: false,
|
|
735
|
+
launchTimer: null,
|
|
736
|
+
pageFallback: pageFallbackApplied ? { requested: pagePath, resolved: resolvedPagePath } : null,
|
|
622
737
|
};
|
|
623
738
|
const rootPage = {
|
|
624
739
|
bridgeId,
|
|
625
740
|
appSessionId,
|
|
626
|
-
pagePath,
|
|
741
|
+
pagePath: resolvedPagePath,
|
|
627
742
|
query: opts.query ?? {},
|
|
628
743
|
isRoot: true,
|
|
629
744
|
isTab,
|
|
@@ -704,16 +819,19 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
704
819
|
};
|
|
705
820
|
appSession.onServiceBoot = bootOnServiceLoad;
|
|
706
821
|
serviceWindow.webContents.on('did-finish-load', bootOnServiceLoad);
|
|
707
|
-
|
|
822
|
+
const spawnUrl = buildServiceHostSpawnUrl({
|
|
708
823
|
bridgeId,
|
|
709
824
|
appId,
|
|
710
|
-
pagePath,
|
|
825
|
+
pagePath: resolvedPagePath,
|
|
711
826
|
pkgRoot,
|
|
712
827
|
root,
|
|
713
828
|
resourceBaseUrl,
|
|
714
829
|
hostEnvSnapshot: hostEnv,
|
|
715
830
|
apiNamespaces,
|
|
716
|
-
})
|
|
831
|
+
});
|
|
832
|
+
void navigateServiceHost(serviceWindow, spawnUrl, {
|
|
833
|
+
onLoadFailed: err => reportServiceHostNavigationFailed(spawnUrl, err),
|
|
834
|
+
});
|
|
717
835
|
}
|
|
718
836
|
else {
|
|
719
837
|
// Fresh window: its only navigation is service.html (issued inside
|
|
@@ -722,10 +840,33 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
722
840
|
void bootServiceHost(state, appSession, ctx);
|
|
723
841
|
});
|
|
724
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);
|
|
725
864
|
return {
|
|
726
865
|
appSessionId,
|
|
727
866
|
bridgeId,
|
|
728
867
|
pagePath,
|
|
868
|
+
resolvedPagePath,
|
|
869
|
+
pageFallbackApplied,
|
|
729
870
|
serviceWcId: serviceWindow.webContents.id,
|
|
730
871
|
resourceBaseUrl,
|
|
731
872
|
manifest,
|
|
@@ -741,6 +882,15 @@ async function handlePageOpen(state, event, opts) {
|
|
|
741
882
|
throw new Error('[bridge-router] PAGE_OPEN rejected: caller not bound to app session');
|
|
742
883
|
}
|
|
743
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
|
+
}
|
|
744
894
|
const bridgeId = opts.bridgeId || newBridgeId();
|
|
745
895
|
const windowConfig = resolvePageWindowConfig(ap.appConfig, pagePath);
|
|
746
896
|
const isTab = isTabPage(ap.appConfig, pagePath);
|
|
@@ -813,6 +963,10 @@ async function bootServiceHost(state, ap, ctx) {
|
|
|
813
963
|
// app's logic.js into the next spawn (the recycled webContents is shared).
|
|
814
964
|
if (state.appSessions.get(ap.appSessionId) !== ap)
|
|
815
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);
|
|
816
970
|
ap.logicInjected = await injectLogicBundle(ap);
|
|
817
971
|
if (!ap.logicInjected) {
|
|
818
972
|
// The compiled logic.js never executed, so `modDefine` registered nothing.
|
|
@@ -821,7 +975,9 @@ async function bootServiceHost(state, ap, ctx) {
|
|
|
821
975
|
// cause (unreachable resource tree / failed compile / unresolved appId).
|
|
822
976
|
// Skip it and surface one actionable diagnostic instead; the render side
|
|
823
977
|
// gates on the same flag in `routeFromRender`.
|
|
824
|
-
reportLogicLoadFailure(ap, ctx);
|
|
978
|
+
const reason = reportLogicLoadFailure(ap, ctx);
|
|
979
|
+
clearLaunchTimer(ap);
|
|
980
|
+
pushRuntimeStatus(ctx, ap, { phase: 'launch-failed', code: 'logic-bundle-unreachable', reason });
|
|
825
981
|
return;
|
|
826
982
|
}
|
|
827
983
|
// A root page absent from the compiled manifest — most commonly a page the
|
|
@@ -837,7 +993,7 @@ async function bootServiceHost(state, ap, ctx) {
|
|
|
837
993
|
const rootPage = ap.pages.get(ap.appSessionId);
|
|
838
994
|
const rootMissing = !!rootPage && !pageInManifest(ap, rootPage.pagePath);
|
|
839
995
|
if (rootMissing) {
|
|
840
|
-
reportPageNotFound(
|
|
996
|
+
reportPageNotFound(ctx, ap.appSessionId, rootPage.pagePath);
|
|
841
997
|
}
|
|
842
998
|
else if (rootPage) {
|
|
843
999
|
// serviceLoaded is flipped only when service responds with
|
|
@@ -899,34 +1055,65 @@ async function injectLogicBundle(ap) {
|
|
|
899
1055
|
/**
|
|
900
1056
|
* Emit one actionable diagnostic when the compiled logic bundle could not be
|
|
901
1057
|
* loaded — the single point where "the app's compiled resource tree is
|
|
902
|
-
* unreachable" is known with certainty.
|
|
903
|
-
*
|
|
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
|
|
904
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.
|
|
905
1069
|
*/
|
|
906
1070
|
function reportLogicLoadFailure(ap, ctx) {
|
|
907
1071
|
const hint = ap.appId === 'unknown'
|
|
908
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.'
|
|
909
1073
|
: '';
|
|
910
|
-
const
|
|
1074
|
+
const shortReason = `[dimina-kit] Failed to load the mini-program logic bundle from ${logicBundleUrl(ap)}.`;
|
|
1075
|
+
const message = `${shortReason} `
|
|
911
1076
|
+ 'The service runtime has no registered modules, so no page can mount.'
|
|
912
1077
|
+ hint
|
|
913
1078
|
+ ` Verify the project compiled successfully and that the resource server serves "${ap.appId}/${ap.root}/".`;
|
|
914
|
-
|
|
1079
|
+
ctx.diagnostics?.report({
|
|
1080
|
+
severity: 'error',
|
|
1081
|
+
code: 'logic-bundle-unreachable',
|
|
1082
|
+
message,
|
|
1083
|
+
appSessionId: ap.appSessionId,
|
|
1084
|
+
});
|
|
915
1085
|
ctx.guestConsole?.emit({ source: 'service', level: 'error', args: [message] });
|
|
1086
|
+
return shortReason;
|
|
916
1087
|
}
|
|
917
1088
|
/** Whether `pagePath` still exists in the app's freshly compiled manifest. */
|
|
918
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;
|
|
919
1097
|
return ap.manifest.pages.includes(normalizePagePath(pagePath));
|
|
920
1098
|
}
|
|
921
1099
|
/**
|
|
922
|
-
* WeChat-devtools-style "page does not exist" diagnostic.
|
|
923
|
-
*
|
|
924
|
-
*
|
|
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.
|
|
925
1106
|
*/
|
|
926
|
-
function reportPageNotFound(
|
|
927
|
-
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. `
|
|
928
1109
|
+ '2. Invoking Page() in async task.';
|
|
929
|
-
|
|
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
|
+
});
|
|
930
1117
|
ctx.guestConsole?.emit({ source: 'service', level: 'error', args: [message] });
|
|
931
1118
|
}
|
|
932
1119
|
function maybeSendResourceLoaded(ap, page) {
|
|
@@ -1005,6 +1192,18 @@ function routeFromRender(state, ap, page, msg, ctx) {
|
|
|
1005
1192
|
handleContainerMsg(ap, page, msg, ctx, state);
|
|
1006
1193
|
}
|
|
1007
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
|
+
}
|
|
1008
1207
|
function handleContainerMsg(ap, page, msg, ctx, state) {
|
|
1009
1208
|
switch (msg.type) {
|
|
1010
1209
|
case 'serviceResourceLoaded':
|
|
@@ -1024,12 +1223,12 @@ function handleContainerMsg(ap, page, msg, ctx, state) {
|
|
|
1024
1223
|
ap.simulatorWc.send(E.DOM_READY, { bridgeId: page.bridgeId });
|
|
1025
1224
|
}
|
|
1026
1225
|
state.emitRenderEvent({ kind: 'domReady', appId: ap.appId, bridgeId: page.bridgeId });
|
|
1226
|
+
markSessionRunning(ctx, ap, page);
|
|
1027
1227
|
break;
|
|
1028
1228
|
case 'invokeAPI':
|
|
1029
1229
|
void handleSimulatorApi(state, ap, page, msg.body, ctx);
|
|
1030
1230
|
break;
|
|
1031
1231
|
case 'serviceHostError': {
|
|
1032
|
-
console.warn('[bridge-router] service host error:', msg.body);
|
|
1033
1232
|
// Forward the error to every registered `wx.onError` listener. (The dimina
|
|
1034
1233
|
// service runtime's App lifecycle is only onLaunch/onShow/onHide, so it
|
|
1035
1234
|
// does not dispatch `App.onError` — `wx.onError` is the supported path.)
|
|
@@ -1037,19 +1236,32 @@ function handleContainerMsg(ap, page, msg, ctx, state) {
|
|
|
1037
1236
|
// during lifecycle/event dispatch here.
|
|
1038
1237
|
const errBody = msg.body;
|
|
1039
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
|
+
});
|
|
1040
1248
|
for (const id of state.appLifecycle.listeners(ap.appSessionId, 'onError')) {
|
|
1041
1249
|
sendCallback(ap, id, errArg);
|
|
1042
1250
|
}
|
|
1043
1251
|
break;
|
|
1044
1252
|
}
|
|
1045
|
-
case 'consoleLog':
|
|
1253
|
+
case 'consoleLog': {
|
|
1046
1254
|
// Native-host console capture: the render-host / service-host guest preloads
|
|
1047
1255
|
// monkeypatch console.* and post each entry here (one case handles both —
|
|
1048
1256
|
// distinguish via msg.body.source). Forward to the console sink (set by the
|
|
1049
1257
|
// automation server under native-host) so it can rebroadcast as App.logAdded.
|
|
1050
1258
|
// Guarded + non-throwing: console capture must never break message routing.
|
|
1051
1259
|
ctx.guestConsole?.emit(msg.body);
|
|
1260
|
+
const body = msg.body;
|
|
1261
|
+
if (body?.source === 'service')
|
|
1262
|
+
reportServiceUncaughtError(ctx, ap, body);
|
|
1052
1263
|
break;
|
|
1264
|
+
}
|
|
1053
1265
|
case 'storageChanged':
|
|
1054
1266
|
// Native-host SYNC storage liveness: the service-host's `setStorageSync`/etc.
|
|
1055
1267
|
// write `localStorage` directly (no main round-trip), so they post the change
|
|
@@ -1153,6 +1365,14 @@ function handleNavBarApi(ap, page, name, params) {
|
|
|
1153
1365
|
sendCallback(ap, params.success, successResult);
|
|
1154
1366
|
sendCallback(ap, params.complete, successResult);
|
|
1155
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
|
+
}
|
|
1156
1376
|
// Shared by handleNavActionApi/handleTabActionApi: both forward a same-shaped
|
|
1157
1377
|
// payload to the simulator window, differing only in the event name and the
|
|
1158
1378
|
// payload's static `name` union. Simulator-destroyed fails the callback
|
|
@@ -1162,11 +1382,44 @@ function sendActionOrFail(ap, eventName, payload, name, params) {
|
|
|
1162
1382
|
ap.simulatorWc.send(eventName, payload);
|
|
1163
1383
|
return;
|
|
1164
1384
|
}
|
|
1165
|
-
|
|
1166
|
-
sendCallback(ap, params.fail, fail);
|
|
1167
|
-
sendCallback(ap, params.complete, fail);
|
|
1385
|
+
failActionCallback(ap, params, `${name}:fail simulator window destroyed`);
|
|
1168
1386
|
}
|
|
1169
|
-
|
|
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;
|
|
1421
|
+
}
|
|
1422
|
+
}
|
|
1170
1423
|
const payload = {
|
|
1171
1424
|
appSessionId: ap.appSessionId,
|
|
1172
1425
|
bridgeId: page.bridgeId,
|
|
@@ -1243,7 +1496,7 @@ async function handleSimulatorApi(state, ap, page, body, ctx) {
|
|
|
1243
1496
|
return;
|
|
1244
1497
|
}
|
|
1245
1498
|
if (NAV_ACTION_NAMES.has(name)) {
|
|
1246
|
-
handleNavActionApi(ap, page, name, params);
|
|
1499
|
+
handleNavActionApi(ap, ctx, page, name, params);
|
|
1247
1500
|
return;
|
|
1248
1501
|
}
|
|
1249
1502
|
if (TAB_ACTION_NAMES.has(name)) {
|
|
@@ -1288,12 +1541,17 @@ function forwardApiCallToSimulator(state, ap, page, name, params) {
|
|
|
1288
1541
|
const requestId = newRequestId();
|
|
1289
1542
|
// Persistent subscriptions (`keep: true`, e.g. audioListen) bind their event
|
|
1290
1543
|
// bridge synchronously but only emit their FIRST response when the first
|
|
1291
|
-
// event fires (which can be well past the
|
|
1544
|
+
// event fires (which can be well past the no-handler window). Arming the
|
|
1292
1545
|
// one-shot timeout would tear the subscription down before it ever delivers,
|
|
1293
1546
|
// so keep calls run without a timeout and are reaped on page/app teardown.
|
|
1294
1547
|
// Recognise persistent subscriptions BY NAME (`audioListen`) too: the service
|
|
1295
1548
|
// host strips the original `keep: true` before forwarding, so `params.keep` is
|
|
1296
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.
|
|
1297
1555
|
const keep = params.keep === true || isPersistentSimulatorApi(name);
|
|
1298
1556
|
const timer = keep
|
|
1299
1557
|
? undefined
|
|
@@ -1308,7 +1566,7 @@ function forwardApiCallToSimulator(state, ap, page, name, params) {
|
|
|
1308
1566
|
const fail = { errMsg: `${pending.name}:fail no handler (timeout)` };
|
|
1309
1567
|
sendCallback(target, pending.callbacks.fail, fail);
|
|
1310
1568
|
sendCallback(target, pending.callbacks.complete, fail);
|
|
1311
|
-
},
|
|
1569
|
+
}, apiCallWatchdogMs(name, params));
|
|
1312
1570
|
state.pendingApiCalls.set(requestId, {
|
|
1313
1571
|
appSessionId: ap.appSessionId,
|
|
1314
1572
|
callbacks,
|
|
@@ -1381,7 +1639,20 @@ function handleApiResponse(state, sender, payload) {
|
|
|
1381
1639
|
sendCallback(ap, pending.callbacks.complete, payload.result ?? { errMsg: `${pending.name}:ok` });
|
|
1382
1640
|
}
|
|
1383
1641
|
else {
|
|
1384
|
-
|
|
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
|
+
};
|
|
1385
1656
|
sendCallback(ap, pending.callbacks.fail, fail);
|
|
1386
1657
|
sendCallback(ap, pending.callbacks.complete, fail);
|
|
1387
1658
|
}
|
|
@@ -1658,6 +1929,10 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1658
1929
|
if (!ap)
|
|
1659
1930
|
return;
|
|
1660
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);
|
|
1661
1936
|
// Remove this session's shutdown-fallback registry entry so the registry does
|
|
1662
1937
|
// not accumulate one stale closure per respawn. Ordered AFTER the delete
|
|
1663
1938
|
// above: dispose() runs the wrapped fn (disposeAppSession again), which
|
|
@@ -1693,7 +1968,14 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
|
|
|
1693
1968
|
}
|
|
1694
1969
|
}
|
|
1695
1970
|
// ── App-config / manifest parsing ───────────────────────────────────────────
|
|
1696
|
-
|
|
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) {
|
|
1697
1979
|
// `resourceBase` is the dir/URL that directly contains `app-config.json`:
|
|
1698
1980
|
// the dev server's `<base><appId>/<root>/` (http) or the local fallback
|
|
1699
1981
|
// server root (also http). Both are HTTP, so a single fetch path covers them.
|
|
@@ -1701,23 +1983,55 @@ async function loadAppConfig(resourceBase) {
|
|
|
1701
1983
|
try {
|
|
1702
1984
|
const res = await fetch(cfgUrl);
|
|
1703
1985
|
if (!res.ok) {
|
|
1986
|
+
const error = new Error(`app-config.json fetch ${res.status} at ${cfgUrl}`);
|
|
1704
1987
|
console.warn(`[bridge-router] no app-config.json at ${cfgUrl} (${res.status})`);
|
|
1988
|
+
onUnreachable?.({ url: cfgUrl, error });
|
|
1705
1989
|
return {};
|
|
1706
1990
|
}
|
|
1707
1991
|
return await res.json();
|
|
1708
1992
|
}
|
|
1709
1993
|
catch (error) {
|
|
1710
1994
|
console.warn('[bridge-router] failed to fetch/parse app-config.json:', error);
|
|
1995
|
+
onUnreachable?.({ url: cfgUrl, error });
|
|
1711
1996
|
return {};
|
|
1712
1997
|
}
|
|
1713
1998
|
}
|
|
1714
1999
|
function buildAppManifest(appConfig, fallbackEntry) {
|
|
1715
2000
|
const entry = appConfig.app?.entryPagePath || fallbackEntry;
|
|
1716
|
-
const
|
|
2001
|
+
const hasCompiledPages = !!appConfig.app?.pages?.length;
|
|
2002
|
+
const pages = hasCompiledPages ? appConfig.app.pages : [entry];
|
|
1717
2003
|
const tabBar = appConfig.app?.tabBar && Array.isArray(appConfig.app.tabBar.list) && appConfig.app.tabBar.list.length > 0
|
|
1718
2004
|
? appConfig.app.tabBar
|
|
1719
2005
|
: undefined;
|
|
1720
|
-
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 };
|
|
1721
2035
|
}
|
|
1722
2036
|
function resolvePageWindowConfig(appConfig, pagePath) {
|
|
1723
2037
|
const normalized = normalizePagePath(pagePath);
|