@dimina-kit/devtools 0.3.2-dev.20260524104239 → 0.3.2-dev.20260525152421
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 +13 -2
- package/dist/main/app/bootstrap.d.ts +9 -0
- package/dist/main/app/bootstrap.js +28 -1
- package/dist/main/index.bundle.js +570 -20
- package/dist/main/services/simulator-temp-files/disk.d.ts +55 -0
- package/dist/main/services/simulator-temp-files/disk.js +215 -0
- package/dist/main/services/simulator-temp-files/fs-channels.d.ts +69 -0
- package/dist/main/services/simulator-temp-files/fs-channels.js +108 -0
- package/dist/main/services/simulator-temp-files/index.d.ts +33 -0
- package/dist/main/services/simulator-temp-files/index.js +160 -0
- package/dist/main/services/simulator-temp-files/request-handler.d.ts +30 -0
- package/dist/main/services/simulator-temp-files/request-handler.js +178 -0
- package/dist/main/services/simulator-temp-files/resolver.d.ts +22 -0
- package/dist/main/services/simulator-temp-files/resolver.js +22 -0
- package/dist/main/services/simulator-temp-files/store.d.ts +14 -0
- package/dist/main/services/simulator-temp-files/store.js +24 -0
- package/dist/preload/runtime/temp-files.d.ts +8 -0
- package/dist/preload/runtime/temp-files.js +34 -0
- package/dist/preload/windows/simulator.js +38 -6
- package/dist/simulator/assets/simulator-CVOFTpSj.js +9 -0
- package/dist/simulator/simulator.html +1 -1
- package/package.json +4 -4
- package/dist/simulator/assets/simulator-DDRBJ6Wa.js +0 -9
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure dispatcher for `difile://` URL requests.
|
|
3
|
+
*
|
|
4
|
+
* Spec: `packages/devtools/docs/file-system.md` §4.3.
|
|
5
|
+
*
|
|
6
|
+
* The shipping implementation in `index.ts` registers a thin
|
|
7
|
+
* `simSession.protocol.handle('difile')` wrapper that delegates here; the race
|
|
8
|
+
* waiter on `_tmp/*` lives in `index.ts` because it owns the IPC lifecycle.
|
|
9
|
+
* For unit tests we assume the bytes are already in the store.
|
|
10
|
+
*
|
|
11
|
+
* Response shape:
|
|
12
|
+
* - 200: full body, with Content-Type, Cache-Control (immutable), ETag
|
|
13
|
+
* - 206: range slice, plus Content-Range
|
|
14
|
+
* - 304: empty body when `If-None-Match` matches the on-disk ETag. Per RFC
|
|
15
|
+
* 9110 §13.1.2 If-None-Match wins over Range.
|
|
16
|
+
* - 404: anything `resolveVPath` rejects, plus disk-side ENOENT and any
|
|
17
|
+
* other I/O error. The protocol handler in `index.ts` translates this
|
|
18
|
+
* into the renderer's network failure surface; we deliberately do not
|
|
19
|
+
* leak errno strings.
|
|
20
|
+
*/
|
|
21
|
+
import { resolveVPath } from '../../../simulator/vpath.js';
|
|
22
|
+
import { readDiskFile } from './disk.js';
|
|
23
|
+
const CACHE_CONTROL = 'public, max-age=31536000, immutable';
|
|
24
|
+
function getHeader(headers, name) {
|
|
25
|
+
if (!headers)
|
|
26
|
+
return undefined;
|
|
27
|
+
const lc = name.toLowerCase();
|
|
28
|
+
for (const k of Object.keys(headers)) {
|
|
29
|
+
if (k.toLowerCase() === lc)
|
|
30
|
+
return headers[k];
|
|
31
|
+
}
|
|
32
|
+
return undefined;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Weak ETag comparison per RFC 9110 §8.8.3.2: strip the optional `W/` prefix
|
|
36
|
+
* from each side before comparing. Both our generated ETags and conditional
|
|
37
|
+
* headers may or may not include the weak prefix; the comparison must accept
|
|
38
|
+
* `W/"abc"` and `"abc"` as equivalent.
|
|
39
|
+
*/
|
|
40
|
+
function etagsMatch(a, b) {
|
|
41
|
+
if (!a)
|
|
42
|
+
return false;
|
|
43
|
+
const stripped = (s) => (s.startsWith('W/') ? s.slice(2) : s);
|
|
44
|
+
return stripped(a.trim()) === stripped(b);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Parse a `Range: bytes=<start>-<end>` header into an inclusive `{start,end}`.
|
|
48
|
+
* Returns `null` if absent or malformed (callers should treat that as "no
|
|
49
|
+
* range" — a full 200 response, not a 416). `totalSize` clamps the end.
|
|
50
|
+
*/
|
|
51
|
+
function parseRange(header, totalSize) {
|
|
52
|
+
if (!header)
|
|
53
|
+
return null;
|
|
54
|
+
const m = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
|
|
55
|
+
if (!m)
|
|
56
|
+
return null;
|
|
57
|
+
const startStr = m[1];
|
|
58
|
+
const endStr = m[2];
|
|
59
|
+
if (startStr === '' && endStr === '')
|
|
60
|
+
return null;
|
|
61
|
+
if (startStr === '') {
|
|
62
|
+
// Suffix range: last N bytes.
|
|
63
|
+
const n = Number(endStr);
|
|
64
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
65
|
+
return null;
|
|
66
|
+
const start = Math.max(0, totalSize - n);
|
|
67
|
+
return { start, end: totalSize - 1 };
|
|
68
|
+
}
|
|
69
|
+
const start = Number(startStr);
|
|
70
|
+
if (!Number.isFinite(start) || start < 0)
|
|
71
|
+
return null;
|
|
72
|
+
const end = endStr === '' ? totalSize - 1 : Number(endStr);
|
|
73
|
+
if (!Number.isFinite(end))
|
|
74
|
+
return null;
|
|
75
|
+
return { start, end };
|
|
76
|
+
}
|
|
77
|
+
function notFound() {
|
|
78
|
+
return new Response(null, { status: 404 });
|
|
79
|
+
}
|
|
80
|
+
function notModified(etag) {
|
|
81
|
+
return new Response(null, {
|
|
82
|
+
status: 304,
|
|
83
|
+
headers: {
|
|
84
|
+
ETag: etag,
|
|
85
|
+
'Cache-Control': CACHE_CONTROL,
|
|
86
|
+
'Access-Control-Allow-Origin': '*',
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
function bufferToBody(bytes) {
|
|
91
|
+
// Copy into a fresh ArrayBuffer so the result is definitely typed as
|
|
92
|
+
// `ArrayBuffer` (not `ArrayBufferLike` / `SharedArrayBuffer`) and is
|
|
93
|
+
// safe to hand to Response without aliasing the Node Buffer pool.
|
|
94
|
+
const out = new ArrayBuffer(bytes.byteLength);
|
|
95
|
+
new Uint8Array(out).set(bytes);
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
function tempBody(bytes, mime) {
|
|
99
|
+
return new Response(bufferToBody(bytes), {
|
|
100
|
+
status: 200,
|
|
101
|
+
headers: {
|
|
102
|
+
'Content-Type': mime,
|
|
103
|
+
'Cache-Control': CACHE_CONTROL,
|
|
104
|
+
'Access-Control-Allow-Origin': '*',
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
export async function handleDifileRequest(ctx, req) {
|
|
109
|
+
const v = resolveVPath(req.url);
|
|
110
|
+
if (!v)
|
|
111
|
+
return notFound();
|
|
112
|
+
if (v.kind === 'tmp') {
|
|
113
|
+
const record = ctx.tempStore.get(req.url);
|
|
114
|
+
if (!record)
|
|
115
|
+
return notFound();
|
|
116
|
+
return tempBody(record.bytes, record.mime || 'application/octet-stream');
|
|
117
|
+
}
|
|
118
|
+
if (!v.realPath)
|
|
119
|
+
return notFound();
|
|
120
|
+
// Disk-backed: _store or usr.
|
|
121
|
+
try {
|
|
122
|
+
// Probe first to learn the ETag/size so If-None-Match can short-circuit
|
|
123
|
+
// without reading the body. The probe also doubles as our existence /
|
|
124
|
+
// permission check, so anything that throws here (ENOENT, EACCES, ...)
|
|
125
|
+
// surfaces as a 404.
|
|
126
|
+
const probe = await readDiskFile(v.realPath);
|
|
127
|
+
const etag = probe.etag;
|
|
128
|
+
const totalSize = probe.totalSize;
|
|
129
|
+
const mime = probe.mime;
|
|
130
|
+
const ifNoneMatch = getHeader(req.headers, 'If-None-Match');
|
|
131
|
+
if (etagsMatch(ifNoneMatch, etag)) {
|
|
132
|
+
return notModified(etag);
|
|
133
|
+
}
|
|
134
|
+
const rangeHeader = getHeader(req.headers, 'Range');
|
|
135
|
+
const range = parseRange(rangeHeader, totalSize);
|
|
136
|
+
if (range) {
|
|
137
|
+
// Range out of bounds → 416 (Range Not Satisfiable) per RFC 9110 §15.4.
|
|
138
|
+
// Distinct from 404 so callers can tell "wrong file" from "wrong slice".
|
|
139
|
+
if (range.start < 0 || range.start >= totalSize || range.end < range.start) {
|
|
140
|
+
return new Response(null, {
|
|
141
|
+
status: 416,
|
|
142
|
+
headers: {
|
|
143
|
+
'Content-Range': `bytes */${totalSize}`,
|
|
144
|
+
'Access-Control-Allow-Origin': '*',
|
|
145
|
+
},
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const clampedEnd = Math.min(range.end, totalSize - 1);
|
|
149
|
+
const sliced = await readDiskFile(v.realPath, { range: { start: range.start, end: clampedEnd } });
|
|
150
|
+
const { bytes } = sliced;
|
|
151
|
+
const lastByte = range.start + bytes.length - 1;
|
|
152
|
+
return new Response(bufferToBody(bytes), {
|
|
153
|
+
status: 206,
|
|
154
|
+
headers: {
|
|
155
|
+
'Content-Type': mime,
|
|
156
|
+
'Content-Length': String(bytes.length),
|
|
157
|
+
'Content-Range': `bytes ${range.start}-${lastByte}/${totalSize}`,
|
|
158
|
+
'Cache-Control': CACHE_CONTROL,
|
|
159
|
+
ETag: etag,
|
|
160
|
+
'Access-Control-Allow-Origin': '*',
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
return new Response(bufferToBody(probe.bytes), {
|
|
165
|
+
status: 200,
|
|
166
|
+
headers: {
|
|
167
|
+
'Content-Type': mime,
|
|
168
|
+
'Cache-Control': CACHE_CONTROL,
|
|
169
|
+
ETag: etag,
|
|
170
|
+
'Access-Control-Allow-Origin': '*',
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
catch {
|
|
175
|
+
return notFound();
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
//# sourceMappingURL=request-handler.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure lookup helper backing the main-process `difile://_tmp/*` protocol
|
|
3
|
+
* handler. Receives the shared {@link TempFileStore} that the
|
|
4
|
+
* `simulator:temp-file:*` IPC channels populate from the renderer.
|
|
5
|
+
*
|
|
6
|
+
* The full URL (including scheme + host) is used as the Map key — the resolver
|
|
7
|
+
* never re-parses or normalises it, so any URL with a wrong scheme or host
|
|
8
|
+
* misses regardless of suffix similarity.
|
|
9
|
+
*/
|
|
10
|
+
export interface TempFileRecord {
|
|
11
|
+
bytes: Buffer;
|
|
12
|
+
mime: string;
|
|
13
|
+
}
|
|
14
|
+
export type TempFileStore = Map<string, TempFileRecord>;
|
|
15
|
+
export declare function resolveTempFile(store: TempFileStore, url: string): {
|
|
16
|
+
status: 200;
|
|
17
|
+
bytes: Buffer;
|
|
18
|
+
mime: string;
|
|
19
|
+
} | {
|
|
20
|
+
status: 404;
|
|
21
|
+
};
|
|
22
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure lookup helper backing the main-process `difile://_tmp/*` protocol
|
|
3
|
+
* handler. Receives the shared {@link TempFileStore} that the
|
|
4
|
+
* `simulator:temp-file:*` IPC channels populate from the renderer.
|
|
5
|
+
*
|
|
6
|
+
* The full URL (including scheme + host) is used as the Map key — the resolver
|
|
7
|
+
* never re-parses or normalises it, so any URL with a wrong scheme or host
|
|
8
|
+
* misses regardless of suffix similarity.
|
|
9
|
+
*/
|
|
10
|
+
export function resolveTempFile(store, url) {
|
|
11
|
+
if (!url.startsWith('difile://_tmp/'))
|
|
12
|
+
return { status: 404 };
|
|
13
|
+
const record = store.get(url);
|
|
14
|
+
if (!record)
|
|
15
|
+
return { status: 404 };
|
|
16
|
+
return {
|
|
17
|
+
status: 200,
|
|
18
|
+
bytes: record.bytes,
|
|
19
|
+
mime: record.mime || 'application/octet-stream',
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
//# sourceMappingURL=resolver.js.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutators for the shared {@link TempFileStore}. The renderer-side bridge
|
|
3
|
+
* forwards every `createTempFilePath` / `registerTempFilePath` /
|
|
4
|
+
* `revokeTempFilePath` / `revokeAllTempFilePaths` call over IPC, and these
|
|
5
|
+
* helpers translate the payloads into Map writes.
|
|
6
|
+
*
|
|
7
|
+
* ArrayBuffer inputs are normalised to Buffer at the boundary so the
|
|
8
|
+
* resolver hot path can return the bytes directly without re-wrapping.
|
|
9
|
+
*/
|
|
10
|
+
import type { TempFileStore } from './resolver.js';
|
|
11
|
+
export declare function registerTempFile(store: TempFileStore, path: string, mime: string, bytes: ArrayBuffer | Buffer): void;
|
|
12
|
+
export declare function revokeTempFile(store: TempFileStore, path: string): void;
|
|
13
|
+
export declare function revokeAllTempFiles(store: TempFileStore): void;
|
|
14
|
+
//# sourceMappingURL=store.d.ts.map
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mutators for the shared {@link TempFileStore}. The renderer-side bridge
|
|
3
|
+
* forwards every `createTempFilePath` / `registerTempFilePath` /
|
|
4
|
+
* `revokeTempFilePath` / `revokeAllTempFilePaths` call over IPC, and these
|
|
5
|
+
* helpers translate the payloads into Map writes.
|
|
6
|
+
*
|
|
7
|
+
* ArrayBuffer inputs are normalised to Buffer at the boundary so the
|
|
8
|
+
* resolver hot path can return the bytes directly without re-wrapping.
|
|
9
|
+
*/
|
|
10
|
+
export function registerTempFile(store, path, mime, bytes) {
|
|
11
|
+
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(new Uint8Array(bytes));
|
|
12
|
+
// delete-then-set so an existing entry's insertion order is refreshed.
|
|
13
|
+
// Without this, a FIFO cap eviction over `store.keys()` could drop a
|
|
14
|
+
// recently re-written path.
|
|
15
|
+
store.delete(path);
|
|
16
|
+
store.set(path, { bytes: buf, mime });
|
|
17
|
+
}
|
|
18
|
+
export function revokeTempFile(store, path) {
|
|
19
|
+
store.delete(path);
|
|
20
|
+
}
|
|
21
|
+
export function revokeAllTempFiles(store) {
|
|
22
|
+
store.clear();
|
|
23
|
+
}
|
|
24
|
+
//# sourceMappingURL=store.js.map
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preload-side bridge that mirrors the simulator's renderer-side temp-file
|
|
3
|
+
* registry into the main-process store via IPC. The main process serves the
|
|
4
|
+
* resulting `difile://devtools/{uuid}` URLs over a protocol handler bound to
|
|
5
|
+
* the simulator session.
|
|
6
|
+
*/
|
|
7
|
+
export declare function installTempFileBridge(): void;
|
|
8
|
+
//# sourceMappingURL=temp-files.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Preload-side bridge that mirrors the simulator's renderer-side temp-file
|
|
3
|
+
* registry into the main-process store via IPC. The main process serves the
|
|
4
|
+
* resulting `difile://devtools/{uuid}` URLs over a protocol handler bound to
|
|
5
|
+
* the simulator session.
|
|
6
|
+
*/
|
|
7
|
+
import { ipcRenderer } from 'electron';
|
|
8
|
+
import { setTempFileSink } from '../../simulator/temp-files.js';
|
|
9
|
+
export function installTempFileBridge() {
|
|
10
|
+
setTempFileSink({
|
|
11
|
+
write(path, blob) {
|
|
12
|
+
blob
|
|
13
|
+
.arrayBuffer()
|
|
14
|
+
.then((bytes) => {
|
|
15
|
+
ipcRenderer.send('simulator:temp-file:write', {
|
|
16
|
+
path,
|
|
17
|
+
mime: blob.type,
|
|
18
|
+
bytes,
|
|
19
|
+
});
|
|
20
|
+
})
|
|
21
|
+
.catch(() => {
|
|
22
|
+
// best effort: blob read failures cannot be surfaced through the
|
|
23
|
+
// sink contract (`write` is sync void), so we swallow them.
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
revoke(path) {
|
|
27
|
+
ipcRenderer.send('simulator:temp-file:revoke', { path });
|
|
28
|
+
},
|
|
29
|
+
revokeAll() {
|
|
30
|
+
ipcRenderer.send('simulator:temp-file:revoke-all');
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=temp-files.js.map
|
|
@@ -270,10 +270,41 @@ function installCustomApisBridge() {
|
|
|
270
270
|
return exposeOnMainWorld("__diminaCustomApis", bridge);
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
// src/preload/runtime/
|
|
273
|
+
// src/preload/runtime/temp-files.ts
|
|
274
274
|
var import_electron3 = require("electron");
|
|
275
|
+
|
|
276
|
+
// src/simulator/temp-files.ts
|
|
277
|
+
var activeSink = null;
|
|
278
|
+
function setTempFileSink(sink) {
|
|
279
|
+
activeSink = sink;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// src/preload/runtime/temp-files.ts
|
|
283
|
+
function installTempFileBridge() {
|
|
284
|
+
setTempFileSink({
|
|
285
|
+
write(path, blob) {
|
|
286
|
+
blob.arrayBuffer().then((bytes) => {
|
|
287
|
+
import_electron3.ipcRenderer.send("simulator:temp-file:write", {
|
|
288
|
+
path,
|
|
289
|
+
mime: blob.type,
|
|
290
|
+
bytes
|
|
291
|
+
});
|
|
292
|
+
}).catch(() => {
|
|
293
|
+
});
|
|
294
|
+
},
|
|
295
|
+
revoke(path) {
|
|
296
|
+
import_electron3.ipcRenderer.send("simulator:temp-file:revoke", { path });
|
|
297
|
+
},
|
|
298
|
+
revokeAll() {
|
|
299
|
+
import_electron3.ipcRenderer.send("simulator:temp-file:revoke-all");
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// src/preload/runtime/host.ts
|
|
305
|
+
var import_electron4 = require("electron");
|
|
275
306
|
function sendToHost(channel, data) {
|
|
276
|
-
|
|
307
|
+
import_electron4.ipcRenderer.sendToHost(channel, data);
|
|
277
308
|
}
|
|
278
309
|
function onHostMessage(channel, handler) {
|
|
279
310
|
let active = true;
|
|
@@ -281,10 +312,10 @@ function onHostMessage(channel, handler) {
|
|
|
281
312
|
if (!active) return;
|
|
282
313
|
handler(...args);
|
|
283
314
|
};
|
|
284
|
-
|
|
315
|
+
import_electron4.ipcRenderer.on(channel, wrapped);
|
|
285
316
|
return () => {
|
|
286
317
|
active = false;
|
|
287
|
-
|
|
318
|
+
import_electron4.ipcRenderer.removeListener(channel, wrapped);
|
|
288
319
|
};
|
|
289
320
|
}
|
|
290
321
|
function safeSerialize(val) {
|
|
@@ -953,11 +984,11 @@ function createWxmlSource() {
|
|
|
953
984
|
}
|
|
954
985
|
|
|
955
986
|
// src/preload/miniapp-snapshot/host.ts
|
|
956
|
-
var
|
|
987
|
+
var import_electron5 = require("electron");
|
|
957
988
|
var ACCESSOR_KEY = "__miniappSnapshot";
|
|
958
989
|
function exposeApi(api) {
|
|
959
990
|
try {
|
|
960
|
-
|
|
991
|
+
import_electron5.contextBridge.exposeInMainWorld(ACCESSOR_KEY, api);
|
|
961
992
|
} catch {
|
|
962
993
|
;
|
|
963
994
|
window.__miniappSnapshot = api;
|
|
@@ -1215,6 +1246,7 @@ function setupApiCompatHook() {
|
|
|
1215
1246
|
setupApiCompatHook();
|
|
1216
1247
|
installSimulatorBridge();
|
|
1217
1248
|
installCustomApisBridge();
|
|
1249
|
+
installTempFileBridge();
|
|
1218
1250
|
installConsoleInstrumentation();
|
|
1219
1251
|
var snapshotHost = createMiniappSnapshotHost();
|
|
1220
1252
|
snapshotHost.register(createAppDataSource());
|