@dimina-kit/devtools 0.4.0-dev.20260706154125 → 0.4.0-dev.20260707110006
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 +1 -1
- package/dist/main/index.bundle.js +129 -40
- package/dist/main/ipc/bridge-router.d.ts +1 -1
- package/dist/main/ipc/bridge-router.js +14 -11
- package/dist/main/ipc/project-fs.d.ts +4 -0
- package/dist/main/ipc/project-fs.js +4 -1
- package/dist/main/runtime/miniapp-runtime.d.ts +4 -3
- package/dist/main/services/fs-watch-sse.d.ts +33 -0
- package/dist/main/services/fs-watch-sse.js +99 -0
- package/dist/main/services/http-json.d.ts +9 -0
- package/dist/main/services/http-json.js +6 -0
- package/dist/main/services/render-inspect/index.d.ts +1 -1
- package/dist/main/services/render-inspect/index.js +1 -1
- package/dist/main/services/service-host-pool/pool.d.ts +4 -4
- package/dist/main/services/service-host-pool/pool.js +5 -5
- package/dist/main/services/simulator-temp-files/disk.d.ts +1 -1
- package/dist/main/services/simulator-temp-files/disk.js +1 -1
- package/dist/main/services/simulator-temp-files/fs-channels.d.ts +1 -1
- package/dist/main/services/simulator-temp-files/fs-channels.js +1 -1
- package/dist/main/services/simulator-temp-files/request-handler.d.ts +1 -1
- package/dist/main/services/simulator-temp-files/request-handler.js +1 -1
- package/dist/main/services/workbench-coi-server.js +26 -7
- package/dist/native-host/render/render.js +36 -18
- package/dist/preload/windows/host-toolbar-runtime.cjs +1 -1
- package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
- package/dist/preload/windows/main.cjs.map +1 -1
- package/dist/shared/vpath.d.ts +1 -1
- package/dist/shared/vpath.js +1 -1
- package/dist/vscode-workbench/assets/__vite-browser-external-D6-QnwSb.js +1 -0
- package/dist/vscode-workbench/assets/{dist-TpGpmMGi.js → dist-OuMzPA6f.js} +3 -3
- package/dist/vscode-workbench/assets/fs-core-worker-urls-CLvv1Q_0.js +1 -0
- package/dist/vscode-workbench/assets/fs-core.worker-VoMLk-PY.js +1 -0
- package/dist/vscode-workbench/assets/fs-query.worker-fXLkQK9l.js +2 -0
- package/dist/vscode-workbench/assets/{iconv-lite-umd-C9tiCAJa.js → iconv-lite-umd-a5yQx8hi.js} +1 -1
- package/dist/vscode-workbench/assets/{index-DJ1HyMZ7.js → index-CVEUw5jw.js} +458 -458
- package/dist/vscode-workbench/assets/{jschardet-DE1jV513.js → jschardet-BYkY0M2Q.js} +1 -1
- package/dist/vscode-workbench/index.html +1 -1
- package/package.json +4 -4
- package/dist/vscode-workbench/assets/__vite-browser-external-CXi6Kz9W.js +0 -1
package/dist/main/app/app.js
CHANGED
|
@@ -356,7 +356,7 @@ export async function createDevtoolsRuntime(config = {}) {
|
|
|
356
356
|
const context = createContext(config, mainWindow, rendererDir);
|
|
357
357
|
// Anchor the main window's renderer as the first Connection. Resources
|
|
358
358
|
// scoped to the main webContents (acquired by later wiring) tear down with
|
|
359
|
-
// it; see packages/electron-deck/docs/foundation.md
|
|
359
|
+
// it; see packages/electron-deck/docs/foundation.md (teardown paths).
|
|
360
360
|
context.connections.acquire(mainWindow.webContents);
|
|
361
361
|
context.registry.add(registerAppIpc(context));
|
|
362
362
|
// Sandboxed project file-system IPC (the renderer-side project:fs:* surface).
|
|
@@ -827,7 +827,7 @@ function registerProjectFsIpc(ctx) {
|
|
|
827
827
|
// src/main/app/app.ts
|
|
828
828
|
import { app as app15, BrowserWindow as BrowserWindow8, nativeImage, session as session4 } from "electron";
|
|
829
829
|
import fs12 from "fs";
|
|
830
|
-
import
|
|
830
|
+
import path24 from "path";
|
|
831
831
|
|
|
832
832
|
// src/main/utils/paths.ts
|
|
833
833
|
import fs3 from "fs";
|
|
@@ -3043,8 +3043,8 @@ function createHostToolbarView(ctx, reconciler, deps) {
|
|
|
3043
3043
|
hide() {
|
|
3044
3044
|
hideHostToolbar();
|
|
3045
3045
|
},
|
|
3046
|
-
setPreloadPath(
|
|
3047
|
-
hostToolbarPreloadOverride =
|
|
3046
|
+
setPreloadPath(path25) {
|
|
3047
|
+
hostToolbarPreloadOverride = path25;
|
|
3048
3048
|
},
|
|
3049
3049
|
setHeightMode(mode) {
|
|
3050
3050
|
if (mode !== "auto" && !(Number.isFinite(mode.fixed) && mode.fixed >= 0)) {
|
|
@@ -5298,7 +5298,7 @@ var ServiceHostPool = class {
|
|
|
5298
5298
|
/**
|
|
5299
5299
|
* Hand a ready window matching `spec` to the caller. If none is ready, fall
|
|
5300
5300
|
* back to synchronously constructing a fresh window — never blocking on warm
|
|
5301
|
-
* (
|
|
5301
|
+
* (see prewarm-webview.md's invariants section). Fallback windows carry `entryId === null` and
|
|
5302
5302
|
* are destroyed (not pooled) on release.
|
|
5303
5303
|
*
|
|
5304
5304
|
* A spec change (different `preloadPath`) tears down all *pooled* entries and
|
|
@@ -5374,7 +5374,7 @@ var ServiceHostPool = class {
|
|
|
5374
5374
|
}
|
|
5375
5375
|
/**
|
|
5376
5376
|
* Re-target the pool. `target` is clamped to `[0, maxPoolSize]`. Pooled
|
|
5377
|
-
* entries beyond the target are disposed OLDEST-FIRST (
|
|
5377
|
+
* entries beyond the target are disposed OLDEST-FIRST (see prewarm-webview.md); in-use
|
|
5378
5378
|
* entries are untouched. Does not warm new entries up (use `init` / refill).
|
|
5379
5379
|
*/
|
|
5380
5380
|
resize(target) {
|
|
@@ -5503,7 +5503,7 @@ var ServiceHostPool = class {
|
|
|
5503
5503
|
}
|
|
5504
5504
|
/**
|
|
5505
5505
|
* Reset a released window back to a clean, blank state. Order matters
|
|
5506
|
-
* (
|
|
5506
|
+
* (see prewarm-webview.md's state-machine and reset-ordering sections): navigate to blank FIRST so the old document
|
|
5507
5507
|
* stops running and in-flight requests are aborted, THEN clear storage — so a
|
|
5508
5508
|
* still-live old page cannot re-populate storage after the clear.
|
|
5509
5509
|
*/
|
|
@@ -10227,13 +10227,13 @@ function createRenderInspector(options = {}) {
|
|
|
10227
10227
|
import { toDisposable as toDisposable6 } from "@dimina-kit/electron-deck/main";
|
|
10228
10228
|
|
|
10229
10229
|
// src/main/services/simulator-temp-files/store.ts
|
|
10230
|
-
function registerTempFile(store,
|
|
10230
|
+
function registerTempFile(store, path25, mime, bytes) {
|
|
10231
10231
|
const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(new Uint8Array(bytes));
|
|
10232
|
-
store.delete(
|
|
10233
|
-
store.set(
|
|
10232
|
+
store.delete(path25);
|
|
10233
|
+
store.set(path25, { bytes: buf, mime });
|
|
10234
10234
|
}
|
|
10235
|
-
function revokeTempFile(store,
|
|
10236
|
-
store.delete(
|
|
10235
|
+
function revokeTempFile(store, path25) {
|
|
10236
|
+
store.delete(path25);
|
|
10237
10237
|
}
|
|
10238
10238
|
function revokeAllTempFiles(store) {
|
|
10239
10239
|
store.clear();
|
|
@@ -10661,10 +10661,10 @@ function setupSimulatorTempFiles(simSession) {
|
|
|
10661
10661
|
const store = /* @__PURE__ */ new Map();
|
|
10662
10662
|
const pendingWaiters = /* @__PURE__ */ new Map();
|
|
10663
10663
|
let disposed = false;
|
|
10664
|
-
function drainWaiters(
|
|
10665
|
-
const list = pendingWaiters.get(
|
|
10664
|
+
function drainWaiters(path25) {
|
|
10665
|
+
const list = pendingWaiters.get(path25);
|
|
10666
10666
|
if (!list) return;
|
|
10667
|
-
pendingWaiters.delete(
|
|
10667
|
+
pendingWaiters.delete(path25);
|
|
10668
10668
|
for (const fn of list) fn();
|
|
10669
10669
|
}
|
|
10670
10670
|
function drainAllWaiters() {
|
|
@@ -10677,15 +10677,15 @@ function setupSimulatorTempFiles(simSession) {
|
|
|
10677
10677
|
const registry = new IpcRegistry(simulatorOnlyPolicy);
|
|
10678
10678
|
registry.on("simulator:temp-file:write", (_event, payload) => {
|
|
10679
10679
|
if (disposed) return;
|
|
10680
|
-
const { path:
|
|
10681
|
-
registerTempFile(store,
|
|
10680
|
+
const { path: path25, mime, bytes } = payload;
|
|
10681
|
+
registerTempFile(store, path25, mime, bytes);
|
|
10682
10682
|
enforceStoreCap(store);
|
|
10683
|
-
drainWaiters(
|
|
10683
|
+
drainWaiters(path25);
|
|
10684
10684
|
});
|
|
10685
10685
|
registry.on("simulator:temp-file:revoke", (_event, payload) => {
|
|
10686
10686
|
if (disposed) return;
|
|
10687
|
-
const { path:
|
|
10688
|
-
revokeTempFile(store,
|
|
10687
|
+
const { path: path25 } = payload;
|
|
10688
|
+
revokeTempFile(store, path25);
|
|
10689
10689
|
});
|
|
10690
10690
|
registry.on("simulator:temp-file:revoke-all", () => {
|
|
10691
10691
|
if (disposed) return;
|
|
@@ -10829,9 +10829,97 @@ function setupSimulatorSessionPolicy() {
|
|
|
10829
10829
|
|
|
10830
10830
|
// src/main/services/workbench-coi-server.ts
|
|
10831
10831
|
import http2 from "node:http";
|
|
10832
|
-
import
|
|
10832
|
+
import nodeFs3 from "node:fs";
|
|
10833
10833
|
import fs11 from "node:fs/promises";
|
|
10834
|
+
import path23 from "node:path";
|
|
10835
|
+
|
|
10836
|
+
// src/main/services/fs-watch-sse.ts
|
|
10837
|
+
import nodeFs2 from "node:fs";
|
|
10834
10838
|
import path22 from "node:path";
|
|
10839
|
+
|
|
10840
|
+
// src/main/services/http-json.ts
|
|
10841
|
+
function jsonRes(res, code, obj) {
|
|
10842
|
+
const body = Buffer.from(JSON.stringify(obj));
|
|
10843
|
+
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8", "Content-Length": body.length });
|
|
10844
|
+
res.end(body);
|
|
10845
|
+
}
|
|
10846
|
+
|
|
10847
|
+
// src/main/services/fs-watch-sse.ts
|
|
10848
|
+
var WATCH_DEBOUNCE_MS = 80;
|
|
10849
|
+
function shouldReportWatchPath(rel) {
|
|
10850
|
+
if (rel === "" || rel.startsWith("..") || path22.isAbsolute(rel)) return false;
|
|
10851
|
+
return !rel.split("/").some((segment) => SKIP_DIRS.has(segment));
|
|
10852
|
+
}
|
|
10853
|
+
function handleFsWatchRequest(req, res, getProjectRoot) {
|
|
10854
|
+
const watchedRoot = getProjectRoot();
|
|
10855
|
+
if (!watchedRoot) {
|
|
10856
|
+
jsonRes(res, 409, { error: "No active project", code: "ENOACTIVE" });
|
|
10857
|
+
return;
|
|
10858
|
+
}
|
|
10859
|
+
let watcher;
|
|
10860
|
+
try {
|
|
10861
|
+
watcher = nodeFs2.watch(watchedRoot, { recursive: true }, onChange);
|
|
10862
|
+
} catch (e) {
|
|
10863
|
+
jsonRes(res, 500, { error: String(e) });
|
|
10864
|
+
return;
|
|
10865
|
+
}
|
|
10866
|
+
res.writeHead(200, {
|
|
10867
|
+
"Content-Type": "text/event-stream; charset=utf-8",
|
|
10868
|
+
"Cache-Control": "no-store",
|
|
10869
|
+
Connection: "keep-alive"
|
|
10870
|
+
});
|
|
10871
|
+
let pending = /* @__PURE__ */ new Set();
|
|
10872
|
+
let debounceTimer = null;
|
|
10873
|
+
let liveCheck = null;
|
|
10874
|
+
let closed = false;
|
|
10875
|
+
function cleanup() {
|
|
10876
|
+
if (closed) return;
|
|
10877
|
+
closed = true;
|
|
10878
|
+
if (debounceTimer) clearTimeout(debounceTimer);
|
|
10879
|
+
if (liveCheck) clearInterval(liveCheck);
|
|
10880
|
+
watcher.close();
|
|
10881
|
+
}
|
|
10882
|
+
function flush() {
|
|
10883
|
+
if (closed || pending.size === 0) return;
|
|
10884
|
+
const paths = [...pending];
|
|
10885
|
+
pending = /* @__PURE__ */ new Set();
|
|
10886
|
+
res.write(`data: ${JSON.stringify({ paths })}
|
|
10887
|
+
|
|
10888
|
+
`);
|
|
10889
|
+
}
|
|
10890
|
+
function onChange(_event, filename) {
|
|
10891
|
+
const rel = filename ? filename.toString().split(path22.sep).join("/") : ".";
|
|
10892
|
+
if (rel !== "." && !shouldReportWatchPath(rel)) return;
|
|
10893
|
+
pending.add(rel);
|
|
10894
|
+
if (!debounceTimer) {
|
|
10895
|
+
debounceTimer = setTimeout(() => {
|
|
10896
|
+
debounceTimer = null;
|
|
10897
|
+
flush();
|
|
10898
|
+
}, WATCH_DEBOUNCE_MS);
|
|
10899
|
+
}
|
|
10900
|
+
}
|
|
10901
|
+
watcher.on("error", () => {
|
|
10902
|
+
if (closed) return;
|
|
10903
|
+
res.write(`data: ${JSON.stringify({ watcherDead: true })}
|
|
10904
|
+
|
|
10905
|
+
`);
|
|
10906
|
+
cleanup();
|
|
10907
|
+
res.end();
|
|
10908
|
+
});
|
|
10909
|
+
liveCheck = setInterval(() => {
|
|
10910
|
+
if (closed) return;
|
|
10911
|
+
if (getProjectRoot() !== watchedRoot) {
|
|
10912
|
+
res.write(`data: ${JSON.stringify({ watcherDead: true })}
|
|
10913
|
+
|
|
10914
|
+
`);
|
|
10915
|
+
cleanup();
|
|
10916
|
+
res.end();
|
|
10917
|
+
}
|
|
10918
|
+
}, WATCH_DEBOUNCE_MS);
|
|
10919
|
+
req.on("close", cleanup);
|
|
10920
|
+
}
|
|
10921
|
+
|
|
10922
|
+
// src/main/services/workbench-coi-server.ts
|
|
10835
10923
|
var MAX_FS_WRITE_BYTES = 32 * 1024 * 1024;
|
|
10836
10924
|
var MIME = {
|
|
10837
10925
|
".html": "text/html; charset=utf-8",
|
|
@@ -10855,8 +10943,8 @@ function setIsolationHeaders(res) {
|
|
|
10855
10943
|
}
|
|
10856
10944
|
function containedStaticPath(root, pathname) {
|
|
10857
10945
|
const rel = pathname.replace(/^\/+/, "") || "index.html";
|
|
10858
|
-
const resolved =
|
|
10859
|
-
if (resolved !== root && !resolved.startsWith(root +
|
|
10946
|
+
const resolved = path23.resolve(root, rel);
|
|
10947
|
+
if (resolved !== root && !resolved.startsWith(root + path23.sep)) return null;
|
|
10860
10948
|
return resolved;
|
|
10861
10949
|
}
|
|
10862
10950
|
function serveStaticFile(res, root, pathname) {
|
|
@@ -10867,23 +10955,23 @@ function serveStaticFile(res, root, pathname) {
|
|
|
10867
10955
|
return;
|
|
10868
10956
|
}
|
|
10869
10957
|
Promise.all([fs11.realpath(root), fs11.realpath(candidate)]).then(([realRoot, realFile]) => {
|
|
10870
|
-
if (realFile !== realRoot && !realFile.startsWith(realRoot +
|
|
10958
|
+
if (realFile !== realRoot && !realFile.startsWith(realRoot + path23.sep)) {
|
|
10871
10959
|
res.writeHead(403);
|
|
10872
10960
|
res.end("Forbidden");
|
|
10873
10961
|
return;
|
|
10874
10962
|
}
|
|
10875
|
-
|
|
10963
|
+
nodeFs3.stat(realFile, (err2, stat) => {
|
|
10876
10964
|
if (err2 || !stat.isFile()) {
|
|
10877
10965
|
res.writeHead(404);
|
|
10878
10966
|
res.end("Not Found");
|
|
10879
10967
|
return;
|
|
10880
10968
|
}
|
|
10881
10969
|
res.writeHead(200, {
|
|
10882
|
-
"Content-Type": MIME[
|
|
10970
|
+
"Content-Type": MIME[path23.extname(realFile)] ?? "application/octet-stream",
|
|
10883
10971
|
"Content-Length": stat.size,
|
|
10884
10972
|
"Cache-Control": "no-store"
|
|
10885
10973
|
});
|
|
10886
|
-
|
|
10974
|
+
nodeFs3.createReadStream(realFile).pipe(res);
|
|
10887
10975
|
});
|
|
10888
10976
|
}).catch(() => {
|
|
10889
10977
|
if (!res.headersSent) {
|
|
@@ -10892,11 +10980,6 @@ function serveStaticFile(res, root, pathname) {
|
|
|
10892
10980
|
}
|
|
10893
10981
|
});
|
|
10894
10982
|
}
|
|
10895
|
-
function jsonRes(res, code, obj) {
|
|
10896
|
-
const body = Buffer.from(JSON.stringify(obj));
|
|
10897
|
-
res.writeHead(code, { "Content-Type": "application/json; charset=utf-8", "Content-Length": body.length });
|
|
10898
|
-
res.end(body);
|
|
10899
|
-
}
|
|
10900
10983
|
var BodyTooLargeError = class extends Error {
|
|
10901
10984
|
constructor() {
|
|
10902
10985
|
super("request body exceeds limit");
|
|
@@ -10952,6 +11035,7 @@ function fsErrorStatus(e) {
|
|
|
10952
11035
|
if (code === "ENOACTIVE") return 409;
|
|
10953
11036
|
if (code === "EACCES" || code === "EINVAL") return 403;
|
|
10954
11037
|
if (code === "ENOENT") return 404;
|
|
11038
|
+
if (code === "EISDIR") return 404;
|
|
10955
11039
|
return 500;
|
|
10956
11040
|
}
|
|
10957
11041
|
async function fsStat(ctx) {
|
|
@@ -10960,7 +11044,8 @@ async function fsStat(ctx) {
|
|
|
10960
11044
|
}
|
|
10961
11045
|
async function fsReaddir(ctx) {
|
|
10962
11046
|
const entries = await readdirWithin(ctx.projectRoot, ctx.rel);
|
|
10963
|
-
|
|
11047
|
+
const visible = entries.filter((e) => !(e.isDirectory() && SKIP_DIRS.has(e.name)));
|
|
11048
|
+
jsonRes(ctx.res, 200, visible.map((e) => [e.name, e.isDirectory() ? 2 : 1]));
|
|
10964
11049
|
}
|
|
10965
11050
|
async function fsRead(ctx) {
|
|
10966
11051
|
const buf = await readFileBufferWithin(ctx.projectRoot, ctx.rel);
|
|
@@ -11032,7 +11117,7 @@ async function listFilesRecursive(dir, base = "") {
|
|
|
11032
11117
|
for (const e of entries) {
|
|
11033
11118
|
const rel = base ? `${base}/${e.name}` : e.name;
|
|
11034
11119
|
if (e.isDirectory()) {
|
|
11035
|
-
out.push(...await listFilesRecursive(
|
|
11120
|
+
out.push(...await listFilesRecursive(path23.join(dir, e.name), rel));
|
|
11036
11121
|
} else if (e.isFile()) {
|
|
11037
11122
|
out.push(rel);
|
|
11038
11123
|
}
|
|
@@ -11049,9 +11134,9 @@ async function readContributedExtensions(extensionsDir) {
|
|
|
11049
11134
|
}
|
|
11050
11135
|
for (const e of entries) {
|
|
11051
11136
|
if (!e.isDirectory()) continue;
|
|
11052
|
-
const extDir =
|
|
11137
|
+
const extDir = path23.join(extensionsDir, e.name);
|
|
11053
11138
|
try {
|
|
11054
|
-
const pkgRaw = await fs11.readFile(
|
|
11139
|
+
const pkgRaw = await fs11.readFile(path23.join(extDir, "package.json"), "utf8");
|
|
11055
11140
|
const files = await listFilesRecursive(extDir);
|
|
11056
11141
|
out.push({ dir: e.name, packageJson: JSON.parse(pkgRaw), files });
|
|
11057
11142
|
} catch {
|
|
@@ -11060,11 +11145,15 @@ async function readContributedExtensions(extensionsDir) {
|
|
|
11060
11145
|
return out;
|
|
11061
11146
|
}
|
|
11062
11147
|
async function startWorkbenchCoiServer(options) {
|
|
11063
|
-
const root =
|
|
11064
|
-
const contribRoot = options.extensionsDir ?
|
|
11148
|
+
const root = path23.resolve(options.rootDir);
|
|
11149
|
+
const contribRoot = options.extensionsDir ? path23.resolve(options.extensionsDir) : null;
|
|
11065
11150
|
const server = http2.createServer((req, res) => {
|
|
11066
11151
|
setIsolationHeaders(res);
|
|
11067
11152
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
11153
|
+
if (url.pathname === "/__fs/watch") {
|
|
11154
|
+
handleFsWatchRequest(req, res, options.getProjectRoot);
|
|
11155
|
+
return;
|
|
11156
|
+
}
|
|
11068
11157
|
if (url.pathname.startsWith("/__fs/")) {
|
|
11069
11158
|
handleFsRequest(req, res, options.getProjectRoot(), url).catch((e) => {
|
|
11070
11159
|
if (!res.headersSent) jsonRes(res, 500, { error: String(e) });
|
|
@@ -11279,7 +11368,7 @@ async function disposeContext(ctx) {
|
|
|
11279
11368
|
function createConfiguredMainWindow(config, rendererDir2) {
|
|
11280
11369
|
const mainWindow = createMainWindow({
|
|
11281
11370
|
title: config.appName ?? "Dimina DevTools",
|
|
11282
|
-
indexHtml:
|
|
11371
|
+
indexHtml: path24.join(rendererDir2, "entries/main/index.html"),
|
|
11283
11372
|
width: config.window?.width,
|
|
11284
11373
|
height: config.window?.height,
|
|
11285
11374
|
minWidth: config.window?.minWidth,
|
|
@@ -11558,8 +11647,8 @@ async function createDevtoolsRuntime(config = {}) {
|
|
|
11558
11647
|
bridge: context.bridge
|
|
11559
11648
|
}));
|
|
11560
11649
|
}
|
|
11561
|
-
const bundleDir = config.editorViewConfig?.bundleDir ??
|
|
11562
|
-
if (!fs12.existsSync(
|
|
11650
|
+
const bundleDir = config.editorViewConfig?.bundleDir ?? path24.join(devtoolsPackageRoot, "dist/vscode-workbench");
|
|
11651
|
+
if (!fs12.existsSync(path24.join(bundleDir, "index.html"))) {
|
|
11563
11652
|
console.warn(
|
|
11564
11653
|
`[workbench] editor bundle not found at ${bundleDir} \u2014 skipping embedded editor assembly`
|
|
11565
11654
|
);
|
|
@@ -108,7 +108,7 @@ export interface BridgeRouterHandle {
|
|
|
108
108
|
*/
|
|
109
109
|
census?(): BridgeResourceCensus;
|
|
110
110
|
/**
|
|
111
|
-
* The flag-gated debugTap (
|
|
111
|
+
* The flag-gated debugTap (see foundation.md) over the bridge message stream. Exposed so a
|
|
112
112
|
* hidden devtools panel / automation can read `.entries()` when
|
|
113
113
|
* `DIMINA_DEBUG_TAP=1`; a no-op snapshot otherwise. Optional so the many
|
|
114
114
|
* partial `BridgeRouterHandle` test mocks don't each have to stub it.
|
|
@@ -63,10 +63,11 @@ const STACK_ID = 'stack_0';
|
|
|
63
63
|
* constant instead of a magic number.
|
|
64
64
|
*/
|
|
65
65
|
export const LAUNCH_TIMEOUT_MS = 20_000;
|
|
66
|
-
/** Hard ceiling on the pre-warm pool, mirroring ServiceHostPool/
|
|
66
|
+
/** Hard ceiling on the pre-warm pool, mirroring ServiceHostPool/prewarm-webview.md. */
|
|
67
67
|
const PREWARM_MAX_POOL_SIZE = 4;
|
|
68
68
|
/**
|
|
69
|
-
* Resolve the pre-warm pool size from env (
|
|
69
|
+
* Resolve the pre-warm pool size from env (see prewarm-webview.md's config
|
|
70
|
+
* section). Returns 0 (OFF) unless
|
|
70
71
|
* `DIMINA_PREWARM_POOL_SIZE` is a positive integer and `DIMINA_PREWARM_DISABLE`
|
|
71
72
|
* is not set. Default OFF — pooling is opt-in so the spawn path is unchanged
|
|
72
73
|
* unless explicitly enabled.
|
|
@@ -82,7 +83,7 @@ function resolvePrewarmPoolSize() {
|
|
|
82
83
|
return 0;
|
|
83
84
|
return Math.min(n, PREWARM_MAX_POOL_SIZE);
|
|
84
85
|
}
|
|
85
|
-
/** debugTap (
|
|
86
|
+
/** debugTap (see foundation.md) is opt-in via `DIMINA_DEBUG_TAP=1` — off everywhere else. */
|
|
86
87
|
function resolveDebugTapEnabled() {
|
|
87
88
|
return process.env.DIMINA_DEBUG_TAP === '1';
|
|
88
89
|
}
|
|
@@ -181,8 +182,8 @@ export function installBridgeRouter(ctx) {
|
|
|
181
182
|
if (prewarmPoolSize > 0) {
|
|
182
183
|
const pool = new ServiceHostPool();
|
|
183
184
|
state.pool = pool;
|
|
184
|
-
// Defer warm-up off the cold-start critical path (
|
|
185
|
-
// idle). Warming eagerly here races main-window startup — it creates an
|
|
185
|
+
// Defer warm-up off the cold-start critical path (see prewarm-webview.md:
|
|
186
|
+
// app.ready + idle). Warming eagerly here races main-window startup — it creates an
|
|
186
187
|
// extra hidden BrowserWindow before the workbench renderer has settled,
|
|
187
188
|
// which made the e2e's first-window/preload-readiness flaky. The timer is
|
|
188
189
|
// cancelled on teardown so it can't warm a disposed pool.
|
|
@@ -451,7 +452,7 @@ export function installBridgeRouter(ctx) {
|
|
|
451
452
|
};
|
|
452
453
|
ipcMain.on(C.DISPOSE, onDispose);
|
|
453
454
|
ctx.registry.add(() => { ipcMain.removeListener(C.DISPOSE, onDispose); });
|
|
454
|
-
// debugTap (
|
|
455
|
+
// debugTap (see foundation.md) ingress recorder — near-free no-op unless DIMINA_DEBUG_TAP=1.
|
|
455
456
|
// Hung on the bridge dispatch chokepoint so the cross-wc message flow is
|
|
456
457
|
// inspectable (the first real consumer of the workbench debugTap primitive).
|
|
457
458
|
const tapIn = (channel, sender, payload) => {
|
|
@@ -753,7 +754,8 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
753
754
|
appSession.pages.set(bridgeId, rootPage);
|
|
754
755
|
bindWc(state.serviceWcIdToAppSessionId, serviceWindow.webContents, appSessionId);
|
|
755
756
|
bindSimulatorWc(state.simulatorWcIdToAppSessionIds, simulatorWc, appSessionId);
|
|
756
|
-
// Track the service-host webContents as a Connection (
|
|
757
|
+
// Track the service-host webContents as a Connection (the soft-reuse
|
|
758
|
+
// teardown path documented in foundation.md) and
|
|
757
759
|
// own this session's serviceWc→appSessionId binding on its CURRENT lifetime
|
|
758
760
|
// segment. On a pooled-window REUSE, `disposeAppSession` calls
|
|
759
761
|
// `connections.reset(serviceWc.id)` → this owned cleanup fires + a fresh
|
|
@@ -761,7 +763,8 @@ async function handleSpawn(state, ctx, event, opts) {
|
|
|
761
763
|
// real destroy the connection closes and fires it too. The cleanup is guarded
|
|
762
764
|
// by `appSessionId` so it only clears a binding that is still THIS session's.
|
|
763
765
|
// (The simulatorWc binding stays manual in `disposeAppSession`: the simulator
|
|
764
|
-
// wc outlives individual app sessions — cross-connection state
|
|
766
|
+
// wc outlives individual app sessions — cross-connection state documented in
|
|
767
|
+
// foundation.md — so it
|
|
765
768
|
// cannot hang off any single session's segment.)
|
|
766
769
|
state.connections.acquire(serviceWindow.webContents).own(() => {
|
|
767
770
|
if (state.serviceWcIdToAppSessionId.get(appSession.serviceWc.id) === appSessionId) {
|
|
@@ -1732,8 +1735,8 @@ function ensureRenderBound(state, sender, bridgeId) {
|
|
|
1732
1735
|
page.renderWc = sender;
|
|
1733
1736
|
state.wcIdToBridgeId.set(sender.id, bridgeId);
|
|
1734
1737
|
// Consolidate the render-guest bookkeeping teardown onto the connection
|
|
1735
|
-
// layer (foundation.md
|
|
1736
|
-
// its own connection
|
|
1738
|
+
// layer (see foundation.md): the render guest is its own webContents =
|
|
1739
|
+
// its own connection; own() ties this cleanup to its lifetime so it
|
|
1737
1740
|
// fires on destroy, replacing the bespoke `once('destroyed')`. acquire() is
|
|
1738
1741
|
// idempotent — re-binding the same sender re-uses its connection.
|
|
1739
1742
|
state.connections.acquire(sender).own(() => {
|
|
@@ -1874,7 +1877,7 @@ function closeSessionPages(state, ap) {
|
|
|
1874
1877
|
// the unpooled path.
|
|
1875
1878
|
async function releaseServiceWindow(state, ap, opts) {
|
|
1876
1879
|
if (ap.poolEntryId !== null && state.pool && !opts.serviceAlreadyClosed) {
|
|
1877
|
-
// Soft reuse (foundation.md
|
|
1880
|
+
// Soft reuse (see foundation.md): the pooled service-host webContents keeps
|
|
1878
1881
|
// its wc.id but is about to run a NEW app session. Reset its Connection
|
|
1879
1882
|
// BEFORE returning it to the pool — dispose this session's owned segment
|
|
1880
1883
|
// (the serviceWc binding cleanup) and swap a fresh one — so an old in-flight
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
2
2
|
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
3
|
+
/** Dependency/VCS/build directories invisible to every project-tree consumer:
|
|
4
|
+
* IPC `listFiles`, the `/__fs/readdir` bridge (so the workbench mirror and the
|
|
5
|
+
* WAL ledger seed never walk into them), and the `/__fs/watch` change stream. */
|
|
6
|
+
export declare const SKIP_DIRS: ReadonlySet<string>;
|
|
3
7
|
/**
|
|
4
8
|
* Synchronous, string-level guard: reject empty root (ENOACTIVE), NUL bytes
|
|
5
9
|
* and empty paths (EINVAL), and lexical `..` escapes (EACCES) by resolving
|
|
@@ -49,7 +49,10 @@ import { validate } from '../utils/ipc-schema.js';
|
|
|
49
49
|
import { ProjectFsChannel } from '../../shared/ipc-channels.js';
|
|
50
50
|
/** Cap for `listFiles` — huge monorepos would otherwise stall the renderer. */
|
|
51
51
|
const LIST_FILE_LIMIT = 5000;
|
|
52
|
-
|
|
52
|
+
/** Dependency/VCS/build directories invisible to every project-tree consumer:
|
|
53
|
+
* IPC `listFiles`, the `/__fs/readdir` bridge (so the workbench mirror and the
|
|
54
|
+
* WAL ledger seed never walk into them), and the `/__fs/watch` change stream. */
|
|
55
|
+
export const SKIP_DIRS = new Set([
|
|
53
56
|
'node_modules', '.git', '.svn', '.hg', 'dist', 'build',
|
|
54
57
|
'.next', '.cache', '.turbo', '.parcel-cache', '.nuxt', '.output', '.vite',
|
|
55
58
|
]);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* MiniappRuntime — the stable, host-facing contract surface
|
|
3
|
-
*
|
|
2
|
+
* MiniappRuntime — the stable, host-facing contract surface documented in
|
|
3
|
+
* host-migration.md's public-contract section.
|
|
4
4
|
*
|
|
5
5
|
* HAND-WRITTEN, not derived from `WorkbenchContext`. A `Pick`-style projection
|
|
6
6
|
* would drag every nested internal service type (ViewManager,
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* - `workspace.openProject` stays writable (no `readonly`) for backward
|
|
19
19
|
* compat: hosts MAY still gate permissions by reassigning it. The preferred
|
|
20
20
|
* path is now the declarative `WorkbenchAppConfig.onBeforeOpenProject` hook
|
|
21
|
-
* (runs before any side effect, throw to veto) — see host-migration.md
|
|
21
|
+
* (runs before any side effect, throw to veto) — see host-migration.md's
|
|
22
|
+
* `onBeforeOpenProject` hook section.
|
|
22
23
|
*
|
|
23
24
|
* `asMiniappRuntime(ctx)` is an identity return — the contract is a typed
|
|
24
25
|
* VIEW onto the live context, not a snapshot/projection. That is what makes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `GET /__fs/watch`: SSE stream of the active project root's disk changes, for
|
|
3
|
+
* the editor's memfs↔disk sync engine (devtools-fs-core-feasibility.md §7).
|
|
4
|
+
* Read-only — it adds no write surface, which is why workbench-coi-server.ts
|
|
5
|
+
* dispatches it before (and outside) the mutating-action guard of the generic
|
|
6
|
+
* `/__fs/*` handler. Split into its own module purely to keep each file within
|
|
7
|
+
* the repo size gate; the COI server is its only consumer.
|
|
8
|
+
*
|
|
9
|
+
* One `fs.watch(root, {recursive:true})` per connection (darwin supports
|
|
10
|
+
* `recursive`), changes batched over an 80ms window and pushed as
|
|
11
|
+
* `data: {"paths":["rel", ...]}` with POSIX-separated root-relative paths.
|
|
12
|
+
* `getProjectRoot` (not a resolved string) is threaded through so a project
|
|
13
|
+
* close/switch mid-stream is detected by re-polling the SAME source
|
|
14
|
+
* `handleFsRequest` uses — no separate close event is invented. Watcher errors
|
|
15
|
+
* (e.g. EMFILE) push `data: {"watcherDead":true}` then end the stream; no
|
|
16
|
+
* active project at connect time is a 409, matching the ENOACTIVE semantics of
|
|
17
|
+
* every other `/__fs` action.
|
|
18
|
+
*/
|
|
19
|
+
import type http from 'node:http';
|
|
20
|
+
/** `false` for build/vcs/dependency churn, the empty string, and any path that
|
|
21
|
+
* (defensively — `fs.watch`'s filename is always root-relative) escapes the root.
|
|
22
|
+
* Matches SKIP_DIRS as a path SEGMENT at any depth — the same set `/__fs/readdir`
|
|
23
|
+
* omits, so the watcher never reports paths the editor mirror cannot see (a
|
|
24
|
+
* prefix match would let `packages/x/node_modules/...` through). The one
|
|
25
|
+
* accepted edge: a plain FILE named exactly like a skip dir (e.g. `dist`)
|
|
26
|
+
* is also unreported, though readdir would list it. */
|
|
27
|
+
declare function shouldReportWatchPath(rel: string): boolean;
|
|
28
|
+
export declare const __testing: {
|
|
29
|
+
shouldReportWatchPath: typeof shouldReportWatchPath;
|
|
30
|
+
};
|
|
31
|
+
export declare function handleFsWatchRequest(req: http.IncomingMessage, res: http.ServerResponse, getProjectRoot: () => string): void;
|
|
32
|
+
export {};
|
|
33
|
+
//# sourceMappingURL=fs-watch-sse.d.ts.map
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import nodeFs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { SKIP_DIRS } from '../ipc/project-fs.js';
|
|
4
|
+
import { jsonRes } from './http-json.js';
|
|
5
|
+
/** Debounce window for batching change notifications (ms). */
|
|
6
|
+
const WATCH_DEBOUNCE_MS = 80;
|
|
7
|
+
/** `false` for build/vcs/dependency churn, the empty string, and any path that
|
|
8
|
+
* (defensively — `fs.watch`'s filename is always root-relative) escapes the root.
|
|
9
|
+
* Matches SKIP_DIRS as a path SEGMENT at any depth — the same set `/__fs/readdir`
|
|
10
|
+
* omits, so the watcher never reports paths the editor mirror cannot see (a
|
|
11
|
+
* prefix match would let `packages/x/node_modules/...` through). The one
|
|
12
|
+
* accepted edge: a plain FILE named exactly like a skip dir (e.g. `dist`)
|
|
13
|
+
* is also unreported, though readdir would list it. */
|
|
14
|
+
function shouldReportWatchPath(rel) {
|
|
15
|
+
if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel))
|
|
16
|
+
return false;
|
|
17
|
+
return !rel.split('/').some((segment) => SKIP_DIRS.has(segment));
|
|
18
|
+
}
|
|
19
|
+
export const __testing = { shouldReportWatchPath };
|
|
20
|
+
export function handleFsWatchRequest(req, res, getProjectRoot) {
|
|
21
|
+
const watchedRoot = getProjectRoot();
|
|
22
|
+
if (!watchedRoot) {
|
|
23
|
+
jsonRes(res, 409, { error: 'No active project', code: 'ENOACTIVE' });
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
let watcher;
|
|
27
|
+
try {
|
|
28
|
+
watcher = nodeFs.watch(watchedRoot, { recursive: true }, onChange);
|
|
29
|
+
}
|
|
30
|
+
catch (e) {
|
|
31
|
+
jsonRes(res, 500, { error: String(e) });
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
res.writeHead(200, {
|
|
35
|
+
'Content-Type': 'text/event-stream; charset=utf-8',
|
|
36
|
+
'Cache-Control': 'no-store',
|
|
37
|
+
Connection: 'keep-alive',
|
|
38
|
+
});
|
|
39
|
+
let pending = new Set();
|
|
40
|
+
let debounceTimer = null;
|
|
41
|
+
let liveCheck = null;
|
|
42
|
+
let closed = false;
|
|
43
|
+
function cleanup() {
|
|
44
|
+
if (closed)
|
|
45
|
+
return;
|
|
46
|
+
closed = true;
|
|
47
|
+
if (debounceTimer)
|
|
48
|
+
clearTimeout(debounceTimer);
|
|
49
|
+
if (liveCheck)
|
|
50
|
+
clearInterval(liveCheck);
|
|
51
|
+
watcher.close();
|
|
52
|
+
}
|
|
53
|
+
function flush() {
|
|
54
|
+
if (closed || pending.size === 0)
|
|
55
|
+
return;
|
|
56
|
+
const paths = [...pending];
|
|
57
|
+
pending = new Set();
|
|
58
|
+
res.write(`data: ${JSON.stringify({ paths })}\n\n`);
|
|
59
|
+
}
|
|
60
|
+
function onChange(_event, filename) {
|
|
61
|
+
// FSEvents overflow delivers a null filename ("something changed,
|
|
62
|
+
// rescan"). Dropping it loses data; report '.' and let the client's
|
|
63
|
+
// watch-batch expansion (wal-audit.ts) reconcile the whole tree.
|
|
64
|
+
const rel = filename ? filename.toString().split(path.sep).join('/') : '.';
|
|
65
|
+
if (rel !== '.' && !shouldReportWatchPath(rel))
|
|
66
|
+
return;
|
|
67
|
+
pending.add(rel);
|
|
68
|
+
if (!debounceTimer) {
|
|
69
|
+
debounceTimer = setTimeout(() => {
|
|
70
|
+
debounceTimer = null;
|
|
71
|
+
flush();
|
|
72
|
+
}, WATCH_DEBOUNCE_MS);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
watcher.on('error', () => {
|
|
76
|
+
if (closed)
|
|
77
|
+
return;
|
|
78
|
+
res.write(`data: ${JSON.stringify({ watcherDead: true })}\n\n`);
|
|
79
|
+
cleanup();
|
|
80
|
+
res.end();
|
|
81
|
+
});
|
|
82
|
+
// The active project root has no dedicated close/switch event to subscribe
|
|
83
|
+
// to; poll the same getter `handleFsRequest` uses, on the debounce cadence,
|
|
84
|
+
// so a closed or switched project tears this stream down promptly. Push
|
|
85
|
+
// `watcherDead` BEFORE ending so the client gets a deterministic
|
|
86
|
+
// end-of-channel signal instead of depending on how its EventSource
|
|
87
|
+
// classifies the reconnect-then-409 that would follow a bare close.
|
|
88
|
+
liveCheck = setInterval(() => {
|
|
89
|
+
if (closed)
|
|
90
|
+
return;
|
|
91
|
+
if (getProjectRoot() !== watchedRoot) {
|
|
92
|
+
res.write(`data: ${JSON.stringify({ watcherDead: true })}\n\n`);
|
|
93
|
+
cleanup();
|
|
94
|
+
res.end();
|
|
95
|
+
}
|
|
96
|
+
}, WATCH_DEBOUNCE_MS);
|
|
97
|
+
req.on('close', cleanup);
|
|
98
|
+
}
|
|
99
|
+
//# sourceMappingURL=fs-watch-sse.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal JSON responder shared by the workbench COI server and its
|
|
3
|
+
* `/__fs/watch` SSE module (fs-watch-sse.ts). Lives in its own module because
|
|
4
|
+
* workbench-coi-server.ts imports fs-watch-sse.ts — either file owning it
|
|
5
|
+
* would force an import cycle for the other.
|
|
6
|
+
*/
|
|
7
|
+
import type http from 'node:http';
|
|
8
|
+
export declare function jsonRes(res: http.ServerResponse, code: number, obj: unknown): void;
|
|
9
|
+
//# sourceMappingURL=http-json.d.ts.map
|
|
@@ -38,7 +38,7 @@ export interface RenderInspectorOptions {
|
|
|
38
38
|
/** Override the injected IIFE source (default: read the built bundle). Test seam. */
|
|
39
39
|
loadSource?: () => string;
|
|
40
40
|
/**
|
|
41
|
-
* Connection registry (foundation.md
|
|
41
|
+
* Connection registry (see foundation.md's teardown-paths section). When provided, the per-wc
|
|
42
42
|
* `injected` bookkeeping is torn down via the wc's connection (deterministic
|
|
43
43
|
* with the rest of that webContents's resources) instead of a bespoke
|
|
44
44
|
* `once('destroyed')`. Optional so focused unit tests can omit it (they fall
|
|
@@ -71,7 +71,7 @@ export function createRenderInspector(options = {}) {
|
|
|
71
71
|
}
|
|
72
72
|
injected.add(wc.id);
|
|
73
73
|
// Consolidate the per-wc bookkeeping teardown onto the connection layer
|
|
74
|
-
// (foundation.md
|
|
74
|
+
// (see foundation.md) when a registry is available; fall back to the
|
|
75
75
|
// bespoke `once('destroyed')` only when omitted (focused unit tests).
|
|
76
76
|
const forget = () => {
|
|
77
77
|
injected.delete(wc.id);
|