@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.
- package/dist/main/app/app.js +8 -1
- package/dist/main/index.bundle.js +286 -161
- package/dist/main/ipc/bridge-router.js +36 -3
- package/dist/main/ipc/simulator.js +1 -21
- package/dist/main/services/simulator-temp-files/index.js +38 -16
- package/dist/main/services/views/miniapp-partition.d.ts +80 -0
- package/dist/main/services/views/miniapp-partition.js +138 -0
- package/dist/main/services/views/simulator-session-policy.d.ts +28 -0
- package/dist/main/services/views/simulator-session-policy.js +82 -0
- package/dist/main/services/views/view-manager.js +22 -9
- package/dist/main/windows/main-window/create.js +1 -27
- package/dist/main/windows/service-host-window/create.d.ts +19 -4
- package/dist/main/windows/service-host-window/create.js +34 -8
- package/dist/preload/windows/main.cjs.map +1 -1
- package/dist/preload/windows/simulator.cjs.map +1 -1
- package/dist/service-host/sync-impls/menu-button-geometry.js +8 -0
- package/dist/service-host/sync-impls/menu-button.js +1 -1
- package/dist/shared/bridge-channels.d.ts +14 -0
- package/dist/shared/bridge-channels.js +27 -0
- package/dist/simulator/assets/{device-shell-vwYSOVrh.js → device-shell-fH2cI-3q.js} +2 -2
- package/dist/simulator/assets/{simulator-DddliUL5.js → simulator-DHylZf9Z.js} +3 -3
- package/dist/simulator/simulator.html +1 -1
- package/package.json +8 -7
|
@@ -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
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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 `
|
|
768
|
-
// session-registered preload/CORS rules with the rest of
|
|
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
|
|
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
|
-
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
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 =
|
|
798
|
-
params.partition =
|
|
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
|
|
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,
|