@dimina-kit/devtools 0.4.0-dev.20260630070008 → 0.4.0-dev.20260630094624

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.
@@ -4731,6 +4731,9 @@ function buildServiceHostSpawnUrl(opts) {
4731
4731
  if (opts.hostEnvSnapshot) {
4732
4732
  url.searchParams.set("hostEnv", encodeURIComponent(JSON.stringify(opts.hostEnvSnapshot)));
4733
4733
  }
4734
+ if (opts.apiNamespaces && opts.apiNamespaces.length > 0) {
4735
+ url.searchParams.set("apiNamespaces", opts.apiNamespaces.join(","));
4736
+ }
4734
4737
  return url.toString();
4735
4738
  }
4736
4739
  function navigateServiceHost(win, url) {
@@ -5984,6 +5987,7 @@ async function handleSpawn(state, ctx, event, opts) {
5984
5987
  const workspaceProjectPath = typeof ctx.workspace.getProjectPath === "function" ? ctx.workspace.getProjectPath() : "";
5985
5988
  const pkgRoot = path16.resolve(opts.pkgRoot || workspaceProjectPath || process.cwd());
5986
5989
  const root = opts.root || "main";
5990
+ const apiNamespaces = ctx.apiNamespaces ?? [];
5987
5991
  let resourceServer = null;
5988
5992
  let resourceBaseUrl;
5989
5993
  if (opts.resourceBaseUrl) {
@@ -6025,7 +6029,8 @@ async function handleSpawn(state, ctx, event, opts) {
6025
6029
  pkgRoot,
6026
6030
  root,
6027
6031
  resourceBaseUrl,
6028
- hostEnvSnapshot: hostEnv
6032
+ hostEnvSnapshot: hostEnv,
6033
+ apiNamespaces
6029
6034
  });
6030
6035
  }
6031
6036
  const appSession = {
@@ -6038,6 +6043,7 @@ async function handleSpawn(state, ctx, event, opts) {
6038
6043
  serviceWc: serviceWindow.webContents,
6039
6044
  simulatorWc,
6040
6045
  serviceLoaded: false,
6046
+ logicInjected: null,
6041
6047
  resourceBaseUrl,
6042
6048
  resourceServer,
6043
6049
  hostEnv,
@@ -6059,6 +6065,7 @@ async function handleSpawn(state, ctx, event, opts) {
6059
6065
  renderWc: null,
6060
6066
  renderLoaded: false,
6061
6067
  resourceLoadedSent: false,
6068
+ renderLoadPending: false,
6062
6069
  windowConfig: rootWindowConfig
6063
6070
  };
6064
6071
  state.appSessions.set(appSessionId, appSession);
@@ -6093,7 +6100,7 @@ async function handleSpawn(state, ctx, event, opts) {
6093
6100
  }
6094
6101
  if (!serviceWindow.webContents.getURL().includes("service.html")) return;
6095
6102
  serviceWindow.webContents.removeListener("did-finish-load", bootOnServiceLoad);
6096
- void bootServiceHost(state, appSession);
6103
+ void bootServiceHost(state, appSession, ctx);
6097
6104
  };
6098
6105
  appSession.onServiceBoot = bootOnServiceLoad;
6099
6106
  serviceWindow.webContents.on("did-finish-load", bootOnServiceLoad);
@@ -6106,12 +6113,13 @@ async function handleSpawn(state, ctx, event, opts) {
6106
6113
  pkgRoot,
6107
6114
  root,
6108
6115
  resourceBaseUrl,
6109
- hostEnvSnapshot: hostEnv
6116
+ hostEnvSnapshot: hostEnv,
6117
+ apiNamespaces
6110
6118
  })
6111
6119
  );
6112
6120
  } else {
6113
6121
  serviceWindow.webContents.once("did-finish-load", () => {
6114
- void bootServiceHost(state, appSession);
6122
+ void bootServiceHost(state, appSession, ctx);
6115
6123
  });
6116
6124
  }
6117
6125
  return {
@@ -6145,6 +6153,7 @@ async function handlePageOpen(state, event, opts) {
6145
6153
  renderWc: null,
6146
6154
  renderLoaded: false,
6147
6155
  resourceLoadedSent: false,
6156
+ renderLoadPending: false,
6148
6157
  windowConfig
6149
6158
  };
6150
6159
  state.pageSessions.set(bridgeId, page);
@@ -6191,23 +6200,49 @@ function handleNavCallback(state, sender, payload) {
6191
6200
  }
6192
6201
  sendCallback(ap, payload.callbacks.complete, result);
6193
6202
  }
6194
- async function bootServiceHost(state, ap) {
6203
+ async function bootServiceHost(state, ap, ctx) {
6195
6204
  if (state.appSessions.get(ap.appSessionId) !== ap) return;
6196
- await injectLogicBundle(ap);
6205
+ ap.logicInjected = await injectLogicBundle(ap);
6206
+ if (!ap.logicInjected) {
6207
+ reportLogicLoadFailure(ap, ctx);
6208
+ return;
6209
+ }
6197
6210
  forwardToService(ap, makeLoadResource(ap, ap.pages.get(ap.appSessionId), "service"));
6211
+ for (const page of ap.pages.values()) {
6212
+ if (page.renderLoadPending) {
6213
+ page.renderLoadPending = false;
6214
+ sendRenderLoadResource(ap, page);
6215
+ }
6216
+ }
6217
+ }
6218
+ function sendRenderLoadResource(ap, page) {
6219
+ if (page.renderWc && !page.renderWc.isDestroyed()) {
6220
+ page.renderWc.send(BRIDGE_CHANNELS.TO_RENDER, { msg: makeLoadResource(ap, page, "render") });
6221
+ }
6222
+ }
6223
+ function logicBundleUrl(ap) {
6224
+ return ap.resourceServer ? new URL("logic.js", ap.resourceBaseUrl).toString() : new URL(`${ap.appId}/${ap.root}/logic.js`, ap.resourceBaseUrl).toString();
6198
6225
  }
6199
6226
  async function injectLogicBundle(ap) {
6200
- const logicUrl = ap.resourceServer ? new URL("logic.js", ap.resourceBaseUrl).toString() : new URL(`${ap.appId}/${ap.root}/logic.js`, ap.resourceBaseUrl).toString();
6227
+ const logicUrl = logicBundleUrl(ap);
6201
6228
  try {
6202
6229
  const res = await fetch(logicUrl);
6203
6230
  if (!res.ok) throw new Error(`logic.js fetch ${res.status} at ${logicUrl}`);
6204
6231
  const logicContent = rewriteSourceMappingUrl(await res.text(), logicUrl);
6205
6232
  await ap.serviceWc.executeJavaScript(`${logicContent}
6206
6233
  //# sourceURL=${logicUrl}`, true);
6234
+ return true;
6207
6235
  } catch (error) {
6208
6236
  console.warn("[bridge-router] unable to inject service logic.js:", error);
6237
+ return false;
6209
6238
  }
6210
6239
  }
6240
+ function reportLogicLoadFailure(ap, ctx) {
6241
+ const hint = ap.appId === "unknown" ? ' appId could not be resolved (it fell back to "unknown") \u2014 the mini-program likely failed to compile or its project manifest/app config is missing.' : "";
6242
+ const message = `[dimina-kit] Failed to load the mini-program logic bundle from ${logicBundleUrl(ap)}. The service runtime has no registered modules, so no page can mount.` + hint + ` Verify the project compiled successfully and that the resource server serves "${ap.appId}/${ap.root}/".`;
6243
+ console.error(message);
6244
+ ctx.guestConsole?.emit({ source: "service", level: "error", args: [message] });
6245
+ }
6211
6246
  function maybeSendResourceLoaded(ap, page) {
6212
6247
  if (page.resourceLoadedSent || !ap.serviceLoaded || !page.renderLoaded) return;
6213
6248
  page.resourceLoadedSent = true;
@@ -6242,9 +6277,12 @@ function routeFromService(state, ap, defaultPage, msg, ctx) {
6242
6277
  }
6243
6278
  function routeFromRender(state, ap, page, msg, ctx) {
6244
6279
  if (msg.type === "renderHostReady") {
6245
- if (page.renderWc && !page.renderWc.isDestroyed()) {
6246
- page.renderWc.send(BRIDGE_CHANNELS.TO_RENDER, { msg: makeLoadResource(ap, page, "render") });
6280
+ if (ap.logicInjected === false) return;
6281
+ if (ap.logicInjected === null) {
6282
+ page.renderLoadPending = true;
6283
+ return;
6247
6284
  }
6285
+ sendRenderLoadResource(ap, page);
6248
6286
  return;
6249
6287
  }
6250
6288
  if (msg.type === "renderResourceLoaded") {
@@ -490,6 +490,11 @@ async function handleSpawn(state, ctx, event, opts) {
490
490
  : '';
491
491
  const pkgRoot = path.resolve(opts.pkgRoot || workspaceProjectPath || process.cwd());
492
492
  const root = opts.root || 'main';
493
+ // Host-config custom API namespaces (WorkbenchContext is the single owner).
494
+ // Threaded into the service-host spawn URL so its preload can install the
495
+ // namespace globals; the simulator-supplied `opts.apiNamespaces` is derived
496
+ // from the same host config and is not authoritative here.
497
+ const apiNamespaces = ctx.apiNamespaces ?? [];
493
498
  // Resource base resolution. Preferred: the simulator-supplied dev-server
494
499
  // origin, which statically serves the compiled `<appId>/<root>/…` tree (same
495
500
  // source the default dimina-fe `<webview>` reads). Fallback (no dev server —
@@ -550,6 +555,7 @@ async function handleSpawn(state, ctx, event, opts) {
550
555
  root,
551
556
  resourceBaseUrl,
552
557
  hostEnvSnapshot: hostEnv,
558
+ apiNamespaces,
553
559
  });
554
560
  }
555
561
  const appSession = {
@@ -562,6 +568,7 @@ async function handleSpawn(state, ctx, event, opts) {
562
568
  serviceWc: serviceWindow.webContents,
563
569
  simulatorWc,
564
570
  serviceLoaded: false,
571
+ logicInjected: null,
565
572
  resourceBaseUrl,
566
573
  resourceServer,
567
574
  hostEnv,
@@ -583,6 +590,7 @@ async function handleSpawn(state, ctx, event, opts) {
583
590
  renderWc: null,
584
591
  renderLoaded: false,
585
592
  resourceLoadedSent: false,
593
+ renderLoadPending: false,
586
594
  windowConfig: rootWindowConfig,
587
595
  };
588
596
  state.appSessions.set(appSessionId, appSession);
@@ -653,7 +661,7 @@ async function handleSpawn(state, ctx, event, opts) {
653
661
  if (!serviceWindow.webContents.getURL().includes('service.html'))
654
662
  return;
655
663
  serviceWindow.webContents.removeListener('did-finish-load', bootOnServiceLoad);
656
- void bootServiceHost(state, appSession);
664
+ void bootServiceHost(state, appSession, ctx);
657
665
  };
658
666
  appSession.onServiceBoot = bootOnServiceLoad;
659
667
  serviceWindow.webContents.on('did-finish-load', bootOnServiceLoad);
@@ -665,13 +673,14 @@ async function handleSpawn(state, ctx, event, opts) {
665
673
  root,
666
674
  resourceBaseUrl,
667
675
  hostEnvSnapshot: hostEnv,
676
+ apiNamespaces,
668
677
  }));
669
678
  }
670
679
  else {
671
680
  // Fresh window: its only navigation is service.html (issued inside
672
681
  // createServiceHostWindow), so the first did-finish-load is the spawn load.
673
682
  serviceWindow.webContents.once('did-finish-load', () => {
674
- void bootServiceHost(state, appSession);
683
+ void bootServiceHost(state, appSession, ctx);
675
684
  });
676
685
  }
677
686
  return {
@@ -707,6 +716,7 @@ async function handlePageOpen(state, event, opts) {
707
716
  renderWc: null,
708
717
  renderLoaded: false,
709
718
  resourceLoadedSent: false,
719
+ renderLoadPending: false,
710
720
  windowConfig,
711
721
  };
712
722
  state.pageSessions.set(bridgeId, page);
@@ -761,26 +771,53 @@ function handleNavCallback(state, sender, payload) {
761
771
  sendCallback(ap, payload.callbacks.complete, result);
762
772
  }
763
773
  // ── Service-host boot & per-page resource handshake ──────────────────────────
764
- async function bootServiceHost(state, ap) {
774
+ async function bootServiceHost(state, ap, ctx) {
765
775
  // Liveness guard: never boot a session that was already disposed. With pooling,
766
776
  // the service window is recycled, so a stale did-finish-load listener from an
767
777
  // early-disposed prior owner could otherwise fire here and inject the wrong
768
778
  // app's logic.js into the next spawn (the recycled webContents is shared).
769
779
  if (state.appSessions.get(ap.appSessionId) !== ap)
770
780
  return;
771
- await injectLogicBundle(ap);
781
+ ap.logicInjected = await injectLogicBundle(ap);
782
+ if (!ap.logicInjected) {
783
+ // The compiled logic.js never executed, so `modDefine` registered nothing.
784
+ // Sending `loadResource` now would run `modRequire('app')` against an empty
785
+ // registry and throw the cryptic `module app not found` — masking the real
786
+ // cause (unreachable resource tree / failed compile / unresolved appId).
787
+ // Skip it and surface one actionable diagnostic instead; the render side
788
+ // gates on the same flag in `routeFromRender`.
789
+ reportLogicLoadFailure(ap, ctx);
790
+ return;
791
+ }
772
792
  // serviceLoaded is flipped only when service responds with
773
793
  // `serviceResourceLoaded`; see handleContainerMsg. Setting it here would
774
794
  // race a per-page `resourceLoaded` ahead of the service-side handler.
775
795
  forwardToService(ap, makeLoadResource(ap, ap.pages.get(ap.appSessionId), 'service'));
796
+ // Flush any render `loadResource` that arrived (renderHostReady) while
797
+ // injection was in-flight — now that the bundle is confirmed present.
798
+ for (const page of ap.pages.values()) {
799
+ if (page.renderLoadPending) {
800
+ page.renderLoadPending = false;
801
+ sendRenderLoadResource(ap, page);
802
+ }
803
+ }
804
+ }
805
+ function sendRenderLoadResource(ap, page) {
806
+ if (page.renderWc && !page.renderWc.isDestroyed()) {
807
+ page.renderWc.send(C.TO_RENDER, { msg: makeLoadResource(ap, page, 'render') });
808
+ }
809
+ }
810
+ /** The compiled logic.js URL `injectLogicBundle` fetches (mode-dependent). */
811
+ function logicBundleUrl(ap) {
812
+ return ap.resourceServer
813
+ ? new URL('logic.js', ap.resourceBaseUrl).toString()
814
+ : new URL(`${ap.appId}/${ap.root}/logic.js`, ap.resourceBaseUrl).toString();
776
815
  }
777
816
  async function injectLogicBundle(ap) {
778
817
  // Fetch the compiled service logic over HTTP from the same base the render
779
818
  // host reads (`<base><appId>/<root>/logic.js`). The fallback local server
780
819
  // serves its root, so `<base>logic.js` resolves there too — both are http.
781
- const logicUrl = ap.resourceServer
782
- ? new URL('logic.js', ap.resourceBaseUrl).toString()
783
- : new URL(`${ap.appId}/${ap.root}/logic.js`, ap.resourceBaseUrl).toString();
820
+ const logicUrl = logicBundleUrl(ap);
784
821
  try {
785
822
  const res = await fetch(logicUrl);
786
823
  if (!res.ok)
@@ -790,11 +827,31 @@ async function injectLogicBundle(ap) {
790
827
  // relative map would 404 and break sourcemapped console frames / Sources.
791
828
  const logicContent = rewriteSourceMappingUrl(await res.text(), logicUrl);
792
829
  await ap.serviceWc.executeJavaScript(`${logicContent}\n//# sourceURL=${logicUrl}`, true);
830
+ return true;
793
831
  }
794
832
  catch (error) {
795
833
  console.warn('[bridge-router] unable to inject service logic.js:', error);
834
+ return false;
796
835
  }
797
836
  }
837
+ /**
838
+ * Emit one actionable diagnostic when the compiled logic bundle could not be
839
+ * loaded — the single point where "the app's compiled resource tree is
840
+ * unreachable" is known with certainty. Goes to the main-process log AND the
841
+ * guest console sink so it surfaces in the devtools Console panel where the
842
+ * developer would otherwise see only the misleading `module app not found`.
843
+ */
844
+ function reportLogicLoadFailure(ap, ctx) {
845
+ const hint = ap.appId === 'unknown'
846
+ ? ' 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.'
847
+ : '';
848
+ const message = `[dimina-kit] Failed to load the mini-program logic bundle from ${logicBundleUrl(ap)}. `
849
+ + 'The service runtime has no registered modules, so no page can mount.'
850
+ + hint
851
+ + ` Verify the project compiled successfully and that the resource server serves "${ap.appId}/${ap.root}/".`;
852
+ console.error(message);
853
+ ctx.guestConsole?.emit({ source: 'service', level: 'error', args: [message] });
854
+ }
798
855
  function maybeSendResourceLoaded(ap, page) {
799
856
  if (page.resourceLoadedSent || !ap.serviceLoaded || !page.renderLoaded)
800
857
  return;
@@ -842,9 +899,21 @@ function routeFromService(state, ap, defaultPage, msg, ctx) {
842
899
  }
843
900
  function routeFromRender(state, ap, page, msg, ctx) {
844
901
  if (msg.type === 'renderHostReady') {
845
- if (page.renderWc && !page.renderWc.isDestroyed()) {
846
- page.renderWc.send(C.TO_RENDER, { msg: makeLoadResource(ap, page, 'render') });
902
+ // Logic injection definitively failed: the render bundle shares the same
903
+ // (unreachable) resource tree, so `loadResource` here would only produce a
904
+ // second cryptic `module <pagePath> not found`. bootServiceHost already
905
+ // surfaced the actionable diagnostic — stay silent.
906
+ if (ap.logicInjected === false)
907
+ return;
908
+ // Injection still in-flight: the render guest fires `renderHostReady` on its
909
+ // own DOMContentLoaded, which routinely beats the async fetch+inject. Hold
910
+ // the render `loadResource` and let `bootServiceHost` flush it once the
911
+ // bundle settles, so a failed bundle never reaches the render side.
912
+ if (ap.logicInjected === null) {
913
+ page.renderLoadPending = true;
914
+ return;
847
915
  }
916
+ sendRenderLoadResource(ap, page);
848
917
  return;
849
918
  }
850
919
  if (msg.type === 'renderResourceLoaded') {
@@ -21,6 +21,13 @@ export interface ServiceHostWindowOptions {
21
21
  root?: string;
22
22
  resourceBaseUrl: string;
23
23
  hostEnvSnapshot?: Record<string, unknown>;
24
+ /**
25
+ * Custom API namespace names (host config `apiNamespaces`, e.g. `['qd']`).
26
+ * Encoded into the spawn URL so the service-host preload can install them as
27
+ * `globalThis.__diminaApiNamespaces` before service.js evaluates — without
28
+ * which page logic referencing `qd.*` throws `ReferenceError: qd is not defined`.
29
+ */
30
+ apiNamespaces?: string[];
24
31
  }
25
32
  /**
26
33
  * Construct an (un-navigated) service-host BrowserWindow. Split out from
@@ -60,6 +60,11 @@ export function buildServiceHostSpawnUrl(opts) {
60
60
  // Without this, sync-impls/system-info.ts falls back to generic defaults.
61
61
  url.searchParams.set('hostEnv', encodeURIComponent(JSON.stringify(opts.hostEnvSnapshot)));
62
62
  }
63
+ if (opts.apiNamespaces && opts.apiNamespaces.length > 0) {
64
+ // CSV of namespace names; the preload reads this back into
65
+ // `globalThis.__diminaApiNamespaces`. URLSearchParams round-trips the comma.
66
+ url.searchParams.set('apiNamespaces', opts.apiNamespaces.join(','));
67
+ }
63
68
  return url.toString();
64
69
  }
65
70
  /**
@@ -49,6 +49,17 @@ Object.defineProperty(globalThis, '__diminaSpawnContext', {
49
49
  configurable: false,
50
50
  })
51
51
 
52
+ // Custom API namespace globals. service.js installs `globalThis[ns]` proxies for
53
+ // `['dd','wx',...apiNamespaces]`, reading the host-configured namespaces from
54
+ // `globalThis.__diminaApiNamespaces` first. Set it here (before service.js
55
+ // evaluates) from the spawn URL's CSV param so page logic referencing e.g.
56
+ // `qd.*` resolves instead of throwing `ReferenceError: qd is not defined`.
57
+ // Absent/empty param → `[]`, leaving only the built-in `dd`/`wx`.
58
+ const apiNamespacesRaw = params.get('apiNamespaces')
59
+ globalThis.__diminaApiNamespaces = apiNamespacesRaw
60
+ ? apiNamespacesRaw.split(',').map((name) => name.trim()).filter(Boolean)
61
+ : []
62
+
52
63
  // Live host-env updates (native-host device dropdown). The binding above is
53
64
  // non-configurable, but the inner object's properties are writable — merge the
54
65
  // pushed metrics into `hostEnvSnapshot` in place. `sync-impls/system-info.ts`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dimina-kit/devtools",
3
- "version": "0.4.0-dev.20260630070008",
3
+ "version": "0.4.0-dev.20260630094624",
4
4
  "description": "Dimina DevTools - modular developer tools for mini app debugging",
5
5
  "keywords": [
6
6
  "dimina",
@@ -85,9 +85,9 @@
85
85
  "chrome-remote-interface": "^0.34.0",
86
86
  "ws": "^8.20.0",
87
87
  "zod": "^4.3.6",
88
- "@dimina-kit/devkit": "0.1.2-dev.20260630070008",
89
- "@dimina-kit/electron-deck": "0.1.0-dev.20260630070008",
90
- "@dimina-kit/view-anchor": "0.1.0-dev.20260630070008"
88
+ "@dimina-kit/devkit": "0.1.2-dev.20260630094624",
89
+ "@dimina-kit/electron-deck": "0.1.0-dev.20260630094624",
90
+ "@dimina-kit/view-anchor": "0.1.0-dev.20260630094624"
91
91
  },
92
92
  "devDependencies": {
93
93
  "@playwright/test": "^1.59.1",