@dimina-kit/devtools 0.3.2-dev.20260610082053 → 0.3.2-dev.20260610114009

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.
@@ -1,13 +1,14 @@
1
1
  import { app, ipcMain, protocol, session as electronSession, webContents } from 'electron';
2
2
  import path from 'node:path';
3
3
  import { pathToFileURL } from 'node:url';
4
- import { BRIDGE_CHANNELS as C, SIMULATOR_EVENTS as E } from '../../shared/bridge-channels.js';
4
+ import { BRIDGE_CHANNELS as C, SIMULATOR_EVENTS as E, deviceInfoToHostEnv } from '../../shared/bridge-channels.js';
5
5
  import { isPersistentSimulatorApi } from '../../shared/simulator-api-metadata.js';
6
6
  import { devtoolsPackageRoot } from '../utils/paths.js';
7
7
  import { createDebugTap } from '@dimina-kit/electron-deck/main';
8
8
  import { startDiminaResourceServer } from '../services/dimina-resource-server.js';
9
9
  import { buildServiceHostSpawnUrl, createServiceHostWindow, navigateServiceHost, serviceHostSpec, } from '../windows/service-host-window/create.js';
10
10
  import { ServiceHostPool } from '../services/service-host-pool/pool.js';
11
+ import { registerMiniappSessionConfigurator, SHARED_MINIAPP_PARTITION, } from '../services/views/miniapp-partition.js';
11
12
  import { createConsoleForwarder } from '../services/console-forward/index.js';
12
13
  import { STORAGE_API_NAMES } from '../services/simulator-storage/index.js';
13
14
  const STACK_ID = 'stack_0';
@@ -417,7 +418,18 @@ async function handleSpawn(state, ctx, event, opts) {
417
418
  resourceServer = await startDiminaResourceServer(path.resolve(pkgRoot, root));
418
419
  resourceBaseUrl = resourceServer.baseUrl;
419
420
  }
420
- const hostEnv = makeHostEnv(opts.hostEnvSnapshot);
421
+ // The selected device (renderer toolbar) is the authoritative source for the
422
+ // logical dims a spawn must report. The simulator-supplied `hostEnvSnapshot`
423
+ // is derived from the device baked into the simulator at BOOT time, so on a
424
+ // RESPAWN after a live device change it still carries the boot device. Layer
425
+ // the live `currentDevice` on top so every spawn/respawn reports the selected
426
+ // device — matching what the live `SetDeviceInfo` HostEnvUpdate pushes to an
427
+ // already-running service host. Pre-selection (null) → simulator snapshot wins.
428
+ const selectedDevice = ctx.bridge?.getDevice?.() ?? null;
429
+ const hostEnv = makeHostEnv({
430
+ ...opts.hostEnvSnapshot,
431
+ ...(selectedDevice ? deviceInfoToHostEnv(selectedDevice) : {}),
432
+ });
421
433
  // app-config.json lives at `<base><appId>/<root>/app-config.json` on the dev
422
434
  // server, or at the local server root for the fallback path.
423
435
  const appConfig = await loadAppConfig(resourceServer ? resourceServer.baseUrl : `${resourceBaseUrl}${appId}/${root}/`);
@@ -1256,7 +1268,7 @@ function installResourceProtocolHandlers(ctx, state) {
1256
1268
  const target = new URL(url.pathname.replace(/^\/+/, '') + url.search, ap.resourceBaseUrl);
1257
1269
  return fetch(target);
1258
1270
  };
1259
- const simulatorSession = electronSession.fromPartition('persist:simulator');
1271
+ const simulatorSession = electronSession.fromPartition(SHARED_MINIAPP_PARTITION);
1260
1272
  try {
1261
1273
  protocol.unhandle('dmb-resource');
1262
1274
  }
@@ -1267,7 +1279,22 @@ function installResourceProtocolHandlers(ctx, state) {
1267
1279
  catch { }
1268
1280
  protocol.handle('dmb-resource', handler);
1269
1281
  simulatorSession.protocol.handle('dmb-resource', handler);
1282
+ // Per-project partition sessions need the SAME resource handler so each
1283
+ // project's render/service can load `dmb-resource://…`. Install on every
1284
+ // miniapp partition (current + future); track installed sessions for teardown.
1285
+ const perProjectSessions = new Set();
1286
+ const unregisterConfigurator = registerMiniappSessionConfigurator((sess) => {
1287
+ if (perProjectSessions.has(sess))
1288
+ return;
1289
+ perProjectSessions.add(sess);
1290
+ try {
1291
+ sess.protocol.unhandle('dmb-resource');
1292
+ }
1293
+ catch { }
1294
+ sess.protocol.handle('dmb-resource', handler);
1295
+ });
1270
1296
  ctx.registry.add(() => {
1297
+ unregisterConfigurator();
1271
1298
  try {
1272
1299
  protocol.unhandle('dmb-resource');
1273
1300
  }
@@ -1276,6 +1303,12 @@ function installResourceProtocolHandlers(ctx, state) {
1276
1303
  simulatorSession.protocol.unhandle('dmb-resource');
1277
1304
  }
1278
1305
  catch { }
1306
+ for (const sess of perProjectSessions) {
1307
+ try {
1308
+ sess.protocol.unhandle('dmb-resource');
1309
+ }
1310
+ catch { }
1311
+ }
1279
1312
  });
1280
1313
  }
1281
1314
  function makeHostEnv(snapshot) {
@@ -1,28 +1,8 @@
1
1
  import { ServiceHostChannel, SimulatorChannel, SimulatorCustomApiChannel } from '../../shared/ipc-channels.js';
2
2
  import { SimulatorAttachNativeSchema, SimulatorCustomApiInvokeSchema, SimulatorResizeSchema, SimulatorSetDeviceInfoSchema, SimulatorSetNativeBoundsSchema, SimulatorSetVisibleSchema, } from '../../shared/ipc-schemas.js';
3
+ import { deviceInfoToHostEnv } from '../../shared/bridge-channels.js';
3
4
  import { validate } from '../utils/ipc-schema.js';
4
5
  import { IpcRegistry } from '../utils/ipc-registry.js';
5
- /**
6
- * Map the renderer's logical device metrics onto the subset of a
7
- * HostEnvSnapshot the service-host window's `getSystemInfoSync` consumes.
8
- * `windowHeight` excludes the status bar (the page area); `windowWidth` has no
9
- * horizontal chrome. Zoom is intentionally absent — it is a display scale, not
10
- * a logical-size change.
11
- */
12
- function deviceInfoToHostEnv(d) {
13
- return {
14
- brand: d.brand,
15
- model: d.model,
16
- system: d.system,
17
- platform: d.platform,
18
- pixelRatio: d.pixelRatio,
19
- screenWidth: d.screenWidth,
20
- screenHeight: d.screenHeight,
21
- windowWidth: d.screenWidth,
22
- windowHeight: Math.max(0, d.screenHeight - d.statusBarHeight),
23
- statusBarHeight: d.statusBarHeight,
24
- };
25
- }
26
6
  export function registerSimulatorIpc(ctx) {
27
7
  return new IpcRegistry(ctx.senderPolicy)
28
8
  .handle(SimulatorChannel.AttachNative, (_, ...args) => {
@@ -31,6 +31,7 @@ import { IpcRegistry } from '../../utils/ipc-registry.js';
31
31
  import { toDisposable } from '@dimina-kit/electron-deck/main';
32
32
  import { registerTempFile, revokeTempFile, revokeAllTempFiles } from './store.js';
33
33
  import { handleDifileRequest } from './request-handler.js';
34
+ import { registerMiniappSessionConfigurator } from '../views/miniapp-partition.js';
34
35
  import { handleFsMkdir, handleFsRead, handleFsReaddir, handleFsStat, handleFsUnlink, handleFsWrite, } from './fs-channels.js';
35
36
  /** Upper bound for in-memory entries; oldest insertion is evicted (FIFO). */
36
37
  const MAX_STORE_ENTRIES = 200;
@@ -63,7 +64,12 @@ export function setupSimulatorTempFiles(simSession) {
63
64
  for (const fn of list)
64
65
  fn();
65
66
  }
66
- const simulatorOnlyPolicy = sender => !sender.isDestroyed() && sender.session === simSession;
67
+ // The protocol handler + IPC channels are shared across every per-project
68
+ // miniapp partition session (each project's render/service runs the same
69
+ // difile:// + temp-file FSM). The base session is always trusted; additional
70
+ // per-project partition sessions register themselves via `installOnSession`.
71
+ const trustedSessions = new Set([simSession]);
72
+ const simulatorOnlyPolicy = sender => !sender.isDestroyed() && trustedSessions.has(sender.session);
67
73
  const registry = new IpcRegistry(simulatorOnlyPolicy);
68
74
  registry.on('simulator:temp-file:write', (_event, payload) => {
69
75
  if (disposed)
@@ -84,15 +90,7 @@ export function setupSimulatorTempFiles(simSession) {
84
90
  return;
85
91
  revokeAllTempFiles(store);
86
92
  });
87
- // Idempotent: a stale handler from a prior setup (e.g. fast app
88
- // re-init in tests) is replaced rather than throwing.
89
- try {
90
- simSession.protocol.unhandle('difile');
91
- }
92
- catch {
93
- // Not previously registered — fine.
94
- }
95
- simSession.protocol.handle('difile', async (req) => {
93
+ const difileHandler = async (req) => {
96
94
  const url = req.url;
97
95
  // Forward any HTTP headers Electron parsed (Range / If-None-Match)
98
96
  // to the pure dispatcher so it can do its own conditional / range
@@ -135,7 +133,28 @@ export function setupSimulatorTempFiles(simSession) {
135
133
  res = await handleDifileRequest(ctx, { url, headers });
136
134
  }
137
135
  return res;
138
- });
136
+ };
137
+ // Install the difile:// protocol handler on a session. Idempotent per
138
+ // session: a stale handler from a prior setup (e.g. fast app re-init in
139
+ // tests) is replaced rather than throwing. Trusts that session's senders for
140
+ // the temp-file / fs IPC channels.
141
+ const installedSessions = new Set();
142
+ function installOnSession(sess) {
143
+ trustedSessions.add(sess);
144
+ if (installedSessions.has(sess))
145
+ return;
146
+ installedSessions.add(sess);
147
+ try {
148
+ sess.protocol.unhandle('difile');
149
+ }
150
+ catch {
151
+ // Not previously registered — fine.
152
+ }
153
+ sess.protocol.handle('difile', difileHandler);
154
+ }
155
+ installOnSession(simSession);
156
+ // Apply to every per-project miniapp partition session (current + future).
157
+ const unregisterConfigurator = registerMiniappSessionConfigurator((sess) => installOnSession(sess));
139
158
  // Phase 1 (P1-7): renderer FSM → main fs operations bridge. The same
140
159
  // simulator-only sender policy applies — registry instance is shared.
141
160
  registry.handle('simulator:fs:read', (_event, payload) => handleFsRead(payload));
@@ -146,13 +165,16 @@ export function setupSimulatorTempFiles(simSession) {
146
165
  registry.handle('simulator:fs:mkdir', (_event, payload) => handleFsMkdir(payload));
147
166
  return toDisposable(async () => {
148
167
  disposed = true;
168
+ unregisterConfigurator();
149
169
  drainAllWaiters();
150
170
  store.clear();
151
- try {
152
- simSession.protocol.unhandle('difile');
153
- }
154
- catch {
155
- // May already have been unhandled by app shutdown.
171
+ for (const sess of installedSessions) {
172
+ try {
173
+ sess.protocol.unhandle('difile');
174
+ }
175
+ catch {
176
+ // May already have been unhandled by app shutdown.
177
+ }
156
178
  }
157
179
  await registry.dispose();
158
180
  });
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Per-project session partition for the native-host miniapp runtime (P0 debt).
3
+ *
4
+ * Historically every project's miniapp webContents — the simulator content
5
+ * WebContentsView, its nested render-host `<webview>` guests, AND the
6
+ * service-host window — shared ONE hard-coded `persist:simulator` Electron
7
+ * session. Cookies / localStorage / cache / IndexedDB written by project A were
8
+ * visible to (and clobbered by) project B; two projects open at once cross-
9
+ * contaminate and "clear storage" on one nukes the other.
10
+ *
11
+ * The fix derives a STABLE per-project partition from the project identity (the
12
+ * miniapp `appId`, which flows in via the simulator URL `?appId=` and the spawn
13
+ * path's `appId`). Same project → same partition (storage survives a relaunch);
14
+ * different projects → different partitions (no cross-contamination). The shape
15
+ * mirrors the IDE state of the art (WeChat's static partition, ByteDance's
16
+ * `persist:miniapp-<id>` dynamic partition).
17
+ *
18
+ * The protocol handlers (`difile://`, `dmb-resource`) and webRequest policies
19
+ * (referer / CORS) that the simulator runtime needs were installed once on the
20
+ * single `persist:simulator` session. With per-project partitions those have to
21
+ * be (re)applied to EACH project session the first time it is used. The setup
22
+ * sites register a partition-agnostic configurator here; `configureMiniappSession`
23
+ * runs every registered configurator exactly once per partition.
24
+ *
25
+ * Partition CLEANUP (reclaiming on-disk `persist:` data) is intentionally NOT
26
+ * done here — leaving the data on disk is the whole point of a `persist:`
27
+ * partition (cache/storage survives a relaunch). That is a separate concern.
28
+ */
29
+ import type { Session } from 'electron';
30
+ /** The legacy single-session partition. Still used by the pre-warm pool (which
31
+ * is intentionally NOT isolation-aware — see `serviceHostSpec`) and as the
32
+ * fallback when a project key cannot be derived. */
33
+ export declare const SHARED_MINIAPP_PARTITION = "persist:simulator";
34
+ /**
35
+ * Derive a stable, filesystem-safe partition key from a project `appId`.
36
+ *
37
+ * `appId` is the project identity (e.g. a `wxapp…` id). We pass through the
38
+ * characters Electron is happy to put in a session partition / on-disk folder
39
+ * name (`[A-Za-z0-9_-]`) verbatim so the common case is human-legible, and fold
40
+ * anything else into a short deterministic hash suffix so two appIds that differ
41
+ * only in stripped characters never collide. The same `appId` always yields the
42
+ * same key (so storage survives a relaunch); different `appId`s yield different
43
+ * keys.
44
+ */
45
+ export declare function miniappPartitionKey(appId: string): string;
46
+ /**
47
+ * The Electron session partition for a project. Returns the SHARED partition
48
+ * when `appId` is empty/unknown (so the runtime still has a session to load on,
49
+ * matching the legacy behavior) and a `persist:miniapp-<key>` partition
50
+ * otherwise.
51
+ */
52
+ export declare function miniappPartition(appId: string | null | undefined): string;
53
+ type SessionConfigurator = (sess: Session, partition: string) => void;
54
+ /**
55
+ * Register a configurator that runs against every miniapp partition session.
56
+ * Already-configured partitions are (re)configured immediately so registration
57
+ * order does not matter. Returns a disposer that unregisters the configurator
58
+ * (it does not undo work already applied to live sessions).
59
+ */
60
+ export declare function registerMiniappSessionConfigurator(fn: SessionConfigurator): () => void;
61
+ /**
62
+ * Ensure a partition's session has every registered configurator applied. Safe
63
+ * to call repeatedly per partition (idempotent — each partition is configured
64
+ * once). Call this before loading project content on `partition`.
65
+ *
66
+ * Returns `null` when there is nothing to configure (no configurator registered)
67
+ * so the partition derivation stays independent of any live Electron session —
68
+ * the partition is a constructor-time fact, not a side effect of touching the
69
+ * session API. Setup sites register configurators at app boot; in unit tests
70
+ * that mock `electron` without a `session` export this short-circuits cleanly.
71
+ */
72
+ export declare function configureMiniappSession(partition: string): Session | null;
73
+ /**
74
+ * Test-only: drop the configurator + configured-partition bookkeeping so each
75
+ * test starts from a clean slate. (Module-level state otherwise leaks across
76
+ * unit tests that mock `electron`.)
77
+ */
78
+ export declare function __resetMiniappSessionConfigForTests(): void;
79
+ export {};
80
+ //# sourceMappingURL=miniapp-partition.d.ts.map
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Per-project session partition for the native-host miniapp runtime (P0 debt).
3
+ *
4
+ * Historically every project's miniapp webContents — the simulator content
5
+ * WebContentsView, its nested render-host `<webview>` guests, AND the
6
+ * service-host window — shared ONE hard-coded `persist:simulator` Electron
7
+ * session. Cookies / localStorage / cache / IndexedDB written by project A were
8
+ * visible to (and clobbered by) project B; two projects open at once cross-
9
+ * contaminate and "clear storage" on one nukes the other.
10
+ *
11
+ * The fix derives a STABLE per-project partition from the project identity (the
12
+ * miniapp `appId`, which flows in via the simulator URL `?appId=` and the spawn
13
+ * path's `appId`). Same project → same partition (storage survives a relaunch);
14
+ * different projects → different partitions (no cross-contamination). The shape
15
+ * mirrors the IDE state of the art (WeChat's static partition, ByteDance's
16
+ * `persist:miniapp-<id>` dynamic partition).
17
+ *
18
+ * The protocol handlers (`difile://`, `dmb-resource`) and webRequest policies
19
+ * (referer / CORS) that the simulator runtime needs were installed once on the
20
+ * single `persist:simulator` session. With per-project partitions those have to
21
+ * be (re)applied to EACH project session the first time it is used. The setup
22
+ * sites register a partition-agnostic configurator here; `configureMiniappSession`
23
+ * runs every registered configurator exactly once per partition.
24
+ *
25
+ * Partition CLEANUP (reclaiming on-disk `persist:` data) is intentionally NOT
26
+ * done here — leaving the data on disk is the whole point of a `persist:`
27
+ * partition (cache/storage survives a relaunch). That is a separate concern.
28
+ */
29
+ import * as electron from 'electron';
30
+ /** The legacy single-session partition. Still used by the pre-warm pool (which
31
+ * is intentionally NOT isolation-aware — see `serviceHostSpec`) and as the
32
+ * fallback when a project key cannot be derived. */
33
+ export const SHARED_MINIAPP_PARTITION = 'persist:simulator';
34
+ const PARTITION_PREFIX = 'persist:miniapp-';
35
+ /**
36
+ * Derive a stable, filesystem-safe partition key from a project `appId`.
37
+ *
38
+ * `appId` is the project identity (e.g. a `wxapp…` id). We pass through the
39
+ * characters Electron is happy to put in a session partition / on-disk folder
40
+ * name (`[A-Za-z0-9_-]`) verbatim so the common case is human-legible, and fold
41
+ * anything else into a short deterministic hash suffix so two appIds that differ
42
+ * only in stripped characters never collide. The same `appId` always yields the
43
+ * same key (so storage survives a relaunch); different `appId`s yield different
44
+ * keys.
45
+ */
46
+ export function miniappPartitionKey(appId) {
47
+ const safe = appId.replace(/[^A-Za-z0-9_-]/g, '');
48
+ // Folding may have dropped distinguishing characters; if the input was not
49
+ // already fully safe, append a deterministic hash of the RAW appId so two
50
+ // distinct appIds can never alias onto the same key.
51
+ if (safe === appId && safe.length > 0)
52
+ return safe;
53
+ const hash = djb2(appId).toString(36);
54
+ return safe.length > 0 ? `${safe}-${hash}` : hash;
55
+ }
56
+ /**
57
+ * The Electron session partition for a project. Returns the SHARED partition
58
+ * when `appId` is empty/unknown (so the runtime still has a session to load on,
59
+ * matching the legacy behavior) and a `persist:miniapp-<key>` partition
60
+ * otherwise.
61
+ */
62
+ export function miniappPartition(appId) {
63
+ if (!appId)
64
+ return SHARED_MINIAPP_PARTITION;
65
+ return `${PARTITION_PREFIX}${miniappPartitionKey(appId)}`;
66
+ }
67
+ /** Small, stable string hash (djb2). Not cryptographic — only needs to be
68
+ * deterministic and collision-resistant enough to disambiguate appIds. */
69
+ function djb2(input) {
70
+ let h = 5381;
71
+ for (let i = 0; i < input.length; i++) {
72
+ h = ((h << 5) + h + input.charCodeAt(i)) >>> 0;
73
+ }
74
+ return h;
75
+ }
76
+ const configurators = new Set();
77
+ const configuredPartitions = new Set();
78
+ /**
79
+ * Register a configurator that runs against every miniapp partition session.
80
+ * Already-configured partitions are (re)configured immediately so registration
81
+ * order does not matter. Returns a disposer that unregisters the configurator
82
+ * (it does not undo work already applied to live sessions).
83
+ */
84
+ export function registerMiniappSessionConfigurator(fn) {
85
+ configurators.add(fn);
86
+ for (const partition of configuredPartitions) {
87
+ try {
88
+ fn(electron.session.fromPartition(partition), partition);
89
+ }
90
+ catch (err) {
91
+ console.warn('[miniapp-partition] configurator failed for', partition, err);
92
+ }
93
+ }
94
+ return () => {
95
+ configurators.delete(fn);
96
+ };
97
+ }
98
+ /**
99
+ * Ensure a partition's session has every registered configurator applied. Safe
100
+ * to call repeatedly per partition (idempotent — each partition is configured
101
+ * once). Call this before loading project content on `partition`.
102
+ *
103
+ * Returns `null` when there is nothing to configure (no configurator registered)
104
+ * so the partition derivation stays independent of any live Electron session —
105
+ * the partition is a constructor-time fact, not a side effect of touching the
106
+ * session API. Setup sites register configurators at app boot; in unit tests
107
+ * that mock `electron` without a `session` export this short-circuits cleanly.
108
+ */
109
+ export function configureMiniappSession(partition) {
110
+ // Record that this partition should be configured even if no configurator is
111
+ // registered YET — a later `registerMiniappSessionConfigurator` back-fills it.
112
+ const alreadyConfigured = configuredPartitions.has(partition);
113
+ configuredPartitions.add(partition);
114
+ if (configurators.size === 0)
115
+ return null;
116
+ const sess = electron.session.fromPartition(partition);
117
+ if (alreadyConfigured)
118
+ return sess;
119
+ for (const fn of configurators) {
120
+ try {
121
+ fn(sess, partition);
122
+ }
123
+ catch (err) {
124
+ console.warn('[miniapp-partition] configurator failed for', partition, err);
125
+ }
126
+ }
127
+ return sess;
128
+ }
129
+ /**
130
+ * Test-only: drop the configurator + configured-partition bookkeeping so each
131
+ * test starts from a clean slate. (Module-level state otherwise leaks across
132
+ * unit tests that mock `electron`.)
133
+ */
134
+ export function __resetMiniappSessionConfigForTests() {
135
+ configurators.clear();
136
+ configuredPartitions.clear();
137
+ }
138
+ //# sourceMappingURL=miniapp-partition.js.map
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Referer + CORS webRequest policy for the simulator runtime's sessions.
3
+ *
4
+ * The native render/service hosts fetch the compiled `<appId>/…` resources
5
+ * cross-origin and need a WeChat-style page-frame `Referer`; this installs that
6
+ * policy on the shared fallback session AND on every per-project
7
+ * `persist:miniapp-<key>` partition (current + future) via the partition
8
+ * configurator registry, so isolated projects load resources identically.
9
+ *
10
+ * TEARDOWN: a `webRequest` listener is per-session and there is exactly one
11
+ * slot per (session, event) — re-installing replaces, never stacks. But the
12
+ * configurator registration itself leaks if discarded: re-creating the
13
+ * WorkbenchApp in the same process would register a second configurator that
14
+ * keeps firing for every future partition. So this returns a {@link Disposable}
15
+ * that unregisters the configurator AND clears the listeners off every session
16
+ * it touched. Wire it into the context registry like any other module.
17
+ */
18
+ import { type Disposable } from '@dimina-kit/electron-deck/main';
19
+ /**
20
+ * Install the simulator referer/CORS policy on the shared fallback session and
21
+ * register a configurator so every per-project partition session gets it too.
22
+ * Returns a disposable that unregisters the configurator and clears the policy
23
+ * off every session it installed on — call its teardown when the owning
24
+ * context is disposed so re-creating the app never leaks a duplicate
25
+ * configurator/listener.
26
+ */
27
+ export declare function setupSimulatorSessionPolicy(): Disposable;
28
+ //# sourceMappingURL=simulator-session-policy.d.ts.map
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Referer + CORS webRequest policy for the simulator runtime's sessions.
3
+ *
4
+ * The native render/service hosts fetch the compiled `<appId>/…` resources
5
+ * cross-origin and need a WeChat-style page-frame `Referer`; this installs that
6
+ * policy on the shared fallback session AND on every per-project
7
+ * `persist:miniapp-<key>` partition (current + future) via the partition
8
+ * configurator registry, so isolated projects load resources identically.
9
+ *
10
+ * TEARDOWN: a `webRequest` listener is per-session and there is exactly one
11
+ * slot per (session, event) — re-installing replaces, never stacks. But the
12
+ * configurator registration itself leaks if discarded: re-creating the
13
+ * WorkbenchApp in the same process would register a second configurator that
14
+ * keeps firing for every future partition. So this returns a {@link Disposable}
15
+ * that unregisters the configurator AND clears the listeners off every session
16
+ * it touched. Wire it into the context registry like any other module.
17
+ */
18
+ import { session } from 'electron';
19
+ import { toDisposable } from '@dimina-kit/electron-deck/main';
20
+ import { getSimulatorServicewechatReferer } from '../simulator/referer.js';
21
+ import { registerMiniappSessionConfigurator, SHARED_MINIAPP_PARTITION, } from './miniapp-partition.js';
22
+ /** Apply the simulator runtime's referer + CORS webRequest policy to one
23
+ * session. Each session installs its own listeners (a webRequest listener is
24
+ * per-session), so this runs once per partition. */
25
+ function applySimulatorWebRequestPolicy(simulatorSession) {
26
+ simulatorSession.webRequest.onBeforeSendHeaders((details, callback) => {
27
+ const forcedReferer = getSimulatorServicewechatReferer();
28
+ if (forcedReferer) {
29
+ details.requestHeaders['Referer'] = forcedReferer;
30
+ }
31
+ callback({ requestHeaders: details.requestHeaders });
32
+ });
33
+ simulatorSession.webRequest.onHeadersReceived((details, callback) => {
34
+ const headers = details.responseHeaders ?? {};
35
+ // CORS for the native render/service hosts to fetch compiled app resources
36
+ // cross-origin. (The COOP/COEP cross-origin-isolation headers were only for
37
+ // the removed default-path SharedArrayBuffer sync Worker — dropped.)
38
+ headers['access-control-allow-origin'] = ['*'];
39
+ headers['access-control-allow-headers'] = ['*'];
40
+ headers['access-control-allow-methods'] = ['*'];
41
+ callback({ responseHeaders: headers });
42
+ });
43
+ }
44
+ /** Remove the policy listeners from a session (passing `null` clears the slot). */
45
+ function clearSimulatorWebRequestPolicy(simulatorSession) {
46
+ try {
47
+ simulatorSession.webRequest.onBeforeSendHeaders(null);
48
+ simulatorSession.webRequest.onHeadersReceived(null);
49
+ }
50
+ catch {
51
+ // Session already gone (app shutdown) — nothing to clear.
52
+ }
53
+ }
54
+ /**
55
+ * Install the simulator referer/CORS policy on the shared fallback session and
56
+ * register a configurator so every per-project partition session gets it too.
57
+ * Returns a disposable that unregisters the configurator and clears the policy
58
+ * off every session it installed on — call its teardown when the owning
59
+ * context is disposed so re-creating the app never leaks a duplicate
60
+ * configurator/listener.
61
+ */
62
+ export function setupSimulatorSessionPolicy() {
63
+ const configured = new Set();
64
+ function install(sess) {
65
+ if (configured.has(sess))
66
+ return;
67
+ configured.add(sess);
68
+ applySimulatorWebRequestPolicy(sess);
69
+ }
70
+ // Shared fallback session (pre-warm pool + unknown-appId path).
71
+ install(session.fromPartition(SHARED_MINIAPP_PARTITION));
72
+ // Every per-project miniapp partition session (current + future) gets the
73
+ // same referer/CORS policy so isolated projects load resources identically.
74
+ const unregister = registerMiniappSessionConfigurator((sess) => install(sess));
75
+ return toDisposable(() => {
76
+ unregister();
77
+ for (const sess of configured)
78
+ clearSimulatorWebRequestPolicy(sess);
79
+ configured.clear();
80
+ });
81
+ }
82
+ //# sourceMappingURL=simulator-session-policy.js.map
@@ -10,6 +10,8 @@ import { installElementsForward } from '../elements-forward/index.js';
10
10
  import * as layout from '../layout/index.js';
11
11
  import { handleCustomApiBridgeRequest, } from '../simulator/custom-apis.js';
12
12
  import { getDefaultTab } from '../workbench-context.js';
13
+ import { configureMiniappSession, miniappPartition } from './miniapp-partition.js';
14
+ import { parseRoute } from '../../../shared/simulator-route.js';
13
15
  /**
14
16
  * Build a ViewManager bound to the given context. The returned object is the
15
17
  * only component allowed to instantiate or add/remove overlay WebContentsViews.
@@ -760,12 +762,22 @@ export function createViewManager(ctx) {
760
762
  catch { /* ignore */ }
761
763
  nativeSimulatorView = null;
762
764
  }
765
+ // Derive THIS project's session partition from the simulator URL's appId so
766
+ // its cookies/localStorage/cache are isolated from every other project (P0
767
+ // debt). Same project → same partition (storage survives a relaunch);
768
+ // unknown appId → the shared fallback. Configure the partition's session
769
+ // (protocol handlers + CORS/referer policy) before any project content loads
770
+ // on it — idempotent per partition.
771
+ const route = parseRoute(simulatorUrl);
772
+ const partition = miniappPartition(route?.appId);
773
+ configureMiniappSession(partition);
763
774
  // The simulator preload is a CJS bundle; webPreferences.preload obeys the
764
775
  // `.js` + "type":"module" ESM rule (require would be undefined), so hand the
765
776
  // top-level WebContentsView the `.cjs` sibling. contextIsolation:false +
766
777
  // sandbox:false + webviewTag:true mirror what the default `<webview>` guest
767
- // runs with, and `partition:'persist:simulator'` shares storage + the
768
- // session-registered preload/CORS rules with the rest of the simulator.
778
+ // runs with, and the per-project `persist:miniapp-<key>` partition shares
779
+ // storage + the session-registered preload/CORS rules with the rest of THIS
780
+ // project's simulator (render guests + service host), never other projects'.
769
781
  const view = new WebContentsView({
770
782
  webPreferences: {
771
783
  nodeIntegration: false,
@@ -773,7 +785,7 @@ export function createViewManager(ctx) {
773
785
  sandbox: false,
774
786
  webviewTag: true,
775
787
  preload: cjsSiblingPreloadPath(ctx.preloadPath),
776
- partition: 'persist:simulator',
788
+ partition,
777
789
  },
778
790
  });
779
791
  nativeSimulatorView = view;
@@ -788,14 +800,15 @@ export function createViewManager(ctx) {
788
800
  // `sendToHost` requests straight to `ctx.simulatorApis` from main.
789
801
  attachNativeCustomApiBridge(simWc);
790
802
  // DeviceShell mounts per-page render-host `<webview>`s INSIDE this view.
791
- // Mirror windows/main-window/create.ts: pin them onto persist:simulator and
792
- // run them with contextIsolation/sandbox off so the render runtime + its
793
- // preload share the page realm. (A top-level WebContentsView can host these
794
- // guests; a `<webview>` guest cannot that's the whole point of Option A.)
803
+ // Pin them onto the SAME per-project partition as their host WCV (so render
804
+ // and the rest of this project share one localStorage/cookie jar) and run
805
+ // them with contextIsolation/sandbox off so the render runtime + its preload
806
+ // share the page realm. (A top-level WebContentsView can host these guests; a
807
+ // `<webview>` guest cannot — that's the whole point of Option A.)
795
808
  simWc.on('will-attach-webview', (_event, webPreferences, params) => {
796
809
  ;
797
- webPreferences.partition = 'persist:simulator';
798
- params.partition = 'persist:simulator';
810
+ webPreferences.partition = partition;
811
+ params.partition = partition;
799
812
  webPreferences.contextIsolation = false;
800
813
  webPreferences.sandbox = false;
801
814
  });
@@ -1,35 +1,9 @@
1
- import { app, BrowserWindow, View, session } from 'electron';
1
+ import { app, BrowserWindow, View } from 'electron';
2
2
  import path from 'path';
3
- import { getSimulatorServicewechatReferer } from '../../services/simulator/referer.js';
4
3
  import { mainPreloadPath } from '../../utils/paths.js';
5
4
  import { themeBg } from '../../utils/theme.js';
6
5
  import { applyNavigationHardening } from '../navigation-hardening.js';
7
- let simulatorSessionConfigured = false;
8
- function configureSimulatorSession() {
9
- if (simulatorSessionConfigured)
10
- return;
11
- simulatorSessionConfigured = true;
12
- const simulatorSession = session.fromPartition('persist:simulator');
13
- simulatorSession.webRequest.onBeforeSendHeaders((details, callback) => {
14
- const forcedReferer = getSimulatorServicewechatReferer();
15
- if (forcedReferer) {
16
- details.requestHeaders['Referer'] = forcedReferer;
17
- }
18
- callback({ requestHeaders: details.requestHeaders });
19
- });
20
- simulatorSession.webRequest.onHeadersReceived((details, callback) => {
21
- const headers = details.responseHeaders ?? {};
22
- // CORS for the native render/service hosts to fetch compiled app resources
23
- // cross-origin. (The COOP/COEP cross-origin-isolation headers were only for
24
- // the removed default-path SharedArrayBuffer sync Worker — dropped.)
25
- headers['access-control-allow-origin'] = ['*'];
26
- headers['access-control-allow-headers'] = ['*'];
27
- headers['access-control-allow-methods'] = ['*'];
28
- callback({ responseHeaders: headers });
29
- });
30
- }
31
6
  export function createMainWindow(opts) {
32
- configureSimulatorSession();
33
7
  const mainWindow = new BrowserWindow({
34
8
  width: opts.width ?? 1280,
35
9
  height: opts.height ?? 980,