@dimina-kit/devtools 0.4.0-dev.20260707070110 → 0.4.0-dev.20260710085051
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 +51 -17
- 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 +32 -3
- package/dist/main/ipc/project-fs.js +33 -5
- package/dist/main/runtime/miniapp-runtime.d.ts +4 -3
- 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/settings/index.d.ts +7 -1
- package/dist/main/services/settings/index.js +22 -2
- 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 +9 -2
- package/dist/main/services/workspace/workspace-service.js +7 -7
- 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 +27 -2
- package/dist/preload/windows/main.cjs.map +2 -2
- package/dist/renderer/assets/workbenchSettings-C6w5S2ZY.js +8 -0
- package/dist/renderer/entries/workbench-settings/index.html +1 -1
- package/dist/shared/ipc-schemas.d.ts +4 -1
- package/dist/shared/ipc-schemas.js +4 -1
- package/dist/shared/vpath.d.ts +1 -1
- package/dist/shared/vpath.js +1 -1
- package/dist/simulator/assets/simulator-A1vGZVBM.js +10 -0
- package/dist/simulator/simulator.html +1 -1
- package/dist/vscode-workbench/assets/__vite-browser-external-B1y6A_ue.js +1 -0
- package/dist/vscode-workbench/assets/{dist-wJUFfrkb.js → dist-UYlh9rWP.js} +3 -3
- package/dist/vscode-workbench/assets/fs-core-worker-urls-BGhWkdqX.js +1 -0
- package/dist/vscode-workbench/assets/fs-core.worker-Bs1XABE2.js +1 -0
- package/dist/vscode-workbench/assets/fs-query.worker-cZ-7Fjch.js +2 -0
- package/dist/vscode-workbench/assets/{iconv-lite-umd-O-GPtKy4.js → iconv-lite-umd-P85lbjJb.js} +1 -1
- package/dist/vscode-workbench/assets/{index-DyxjKeLl.js → index-nrKFi46w.js} +450 -450
- package/dist/vscode-workbench/assets/{jschardet-BorbxV40.js → jschardet-CLQONAKz.js} +1 -1
- package/dist/vscode-workbench/index.html +1 -1
- package/package.json +4 -4
- package/dist/renderer/assets/workbenchSettings-Cr4EHo5w.js +0 -8
- package/dist/simulator/assets/simulator-DU3-fS3v.js +0 -10
- package/dist/vscode-workbench/assets/__vite-browser-external-CjtF3dcZ.js +0 -1
- package/dist/vscode-workbench/assets/fs-core-worker-urls-CLvv1Q_0.js +0 -1
- package/dist/vscode-workbench/assets/fs-core.worker-VoMLk-PY.js +0 -1
- package/dist/vscode-workbench/assets/fs-query.worker-fXLkQK9l.js +0 -2
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).
|
|
@@ -27,7 +27,10 @@ var DEFAULTS = {
|
|
|
27
27
|
port: 7789
|
|
28
28
|
},
|
|
29
29
|
compile: {
|
|
30
|
-
|
|
30
|
+
autoBuild: true
|
|
31
|
+
},
|
|
32
|
+
preview: {
|
|
33
|
+
autoReload: true
|
|
31
34
|
},
|
|
32
35
|
theme: "system",
|
|
33
36
|
lastCreateBaseDir: null
|
|
@@ -35,6 +38,9 @@ var DEFAULTS = {
|
|
|
35
38
|
function getSettingsFile() {
|
|
36
39
|
return path.join(app.getPath("userData"), "dimina-workbench-settings.json");
|
|
37
40
|
}
|
|
41
|
+
function pickBool(value, fallback) {
|
|
42
|
+
return typeof value === "boolean" ? value : fallback;
|
|
43
|
+
}
|
|
38
44
|
function loadWorkbenchSettings() {
|
|
39
45
|
try {
|
|
40
46
|
const data = JSON.parse(fs.readFileSync(getSettingsFile(), "utf-8"));
|
|
@@ -49,7 +55,16 @@ function loadWorkbenchSettings() {
|
|
|
49
55
|
port: data.mcp?.port ?? DEFAULTS.mcp.port
|
|
50
56
|
},
|
|
51
57
|
compile: {
|
|
52
|
-
|
|
58
|
+
// Legacy files persisted a single `compile.watch`; the new schema
|
|
59
|
+
// splits it into `compile.autoBuild` (recompile on save) and
|
|
60
|
+
// `preview.autoReload` (reload the simulator afterwards). Prefer the new
|
|
61
|
+
// key, fall back to the legacy one so an old settings file keeps its
|
|
62
|
+
// "auto-compile" choice; `preview.autoReload` defaults to true (the old
|
|
63
|
+
// always-reload behavior) when the file predates the split.
|
|
64
|
+
autoBuild: pickBool(data.compile?.autoBuild, pickBool(data.compile?.watch, DEFAULTS.compile.autoBuild))
|
|
65
|
+
},
|
|
66
|
+
preview: {
|
|
67
|
+
autoReload: pickBool(data.preview?.autoReload, DEFAULTS.preview.autoReload)
|
|
53
68
|
},
|
|
54
69
|
theme,
|
|
55
70
|
lastCreateBaseDir: typeof data.lastCreateBaseDir === "string" ? data.lastCreateBaseDir : DEFAULTS.lastCreateBaseDir
|
|
@@ -59,7 +74,8 @@ function loadWorkbenchSettings() {
|
|
|
59
74
|
...DEFAULTS,
|
|
60
75
|
cdp: { ...DEFAULTS.cdp },
|
|
61
76
|
mcp: { ...DEFAULTS.mcp },
|
|
62
|
-
compile: { ...DEFAULTS.compile }
|
|
77
|
+
compile: { ...DEFAULTS.compile },
|
|
78
|
+
preview: { ...DEFAULTS.preview }
|
|
63
79
|
};
|
|
64
80
|
}
|
|
65
81
|
}
|
|
@@ -502,12 +518,10 @@ var UpdateChannel = {
|
|
|
502
518
|
};
|
|
503
519
|
|
|
504
520
|
// src/main/ipc/project-fs.ts
|
|
521
|
+
import { WATCH_IGNORE_DIRS } from "@dimina-kit/devkit/watch-ignore";
|
|
505
522
|
var LIST_FILE_LIMIT = 5e3;
|
|
506
523
|
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
507
|
-
|
|
508
|
-
".git",
|
|
509
|
-
".svn",
|
|
510
|
-
".hg",
|
|
524
|
+
...WATCH_IGNORE_DIRS,
|
|
511
525
|
"dist",
|
|
512
526
|
"build",
|
|
513
527
|
".next",
|
|
@@ -665,7 +679,18 @@ async function statWithin(root, rel) {
|
|
|
665
679
|
}
|
|
666
680
|
async function readdirWithin(root, rel) {
|
|
667
681
|
const safe = await resolveWithinProjectRoot(joinRel(root, rel), root);
|
|
668
|
-
|
|
682
|
+
const dirents = await fs2.readdir(safe, { withFileTypes: true });
|
|
683
|
+
return Promise.all(
|
|
684
|
+
dirents.map(async (d) => {
|
|
685
|
+
if (d.isDirectory()) return { name: d.name, isDirectory: true };
|
|
686
|
+
try {
|
|
687
|
+
const st = await fs2.stat(path2.join(safe, d.name));
|
|
688
|
+
return { name: d.name, isDirectory: false, size: st.size, mtimeMs: st.mtimeMs };
|
|
689
|
+
} catch {
|
|
690
|
+
return { name: d.name, isDirectory: false };
|
|
691
|
+
}
|
|
692
|
+
})
|
|
693
|
+
);
|
|
669
694
|
}
|
|
670
695
|
async function readFileBufferWithin(root, rel) {
|
|
671
696
|
return readFileBuffer(root, joinRel(root, rel));
|
|
@@ -4095,16 +4120,18 @@ function createWorkspaceService(ctx) {
|
|
|
4095
4120
|
}
|
|
4096
4121
|
}
|
|
4097
4122
|
async function runCompile(projectPath, sessionGeneration) {
|
|
4098
|
-
const { compile } = loadWorkbenchSettings();
|
|
4123
|
+
const { compile, preview } = loadWorkbenchSettings();
|
|
4099
4124
|
try {
|
|
4100
4125
|
const session5 = await ctx.adapter.openProject({
|
|
4101
4126
|
projectPath,
|
|
4102
4127
|
sourcemap: true,
|
|
4103
4128
|
fileTypes: ctx.fileTypes,
|
|
4104
|
-
|
|
4129
|
+
// Two independent gates: autoBuild = recompile on save; autoReload = refresh the simulator afterwards (off ⇒ page/form state survives; hotReload below stays false so the native simulator, which has no SSE reload, isn't refreshed).
|
|
4130
|
+
watch: compile.autoBuild,
|
|
4131
|
+
autoReload: preview.autoReload,
|
|
4105
4132
|
onRebuild: () => {
|
|
4106
4133
|
const { pages } = getProjectPages(projectPath);
|
|
4107
|
-
sendStatus("ready", "\u7F16\u8BD1\u5B8C\u6210\uFF0C\u5DF2\u91CD\u542F",
|
|
4134
|
+
sendStatus("ready", preview.autoReload ? "\u7F16\u8BD1\u5B8C\u6210\uFF0C\u5DF2\u91CD\u542F" : "\u7F16\u8BD1\u5B8C\u6210", preview.autoReload, pages.length ? pages : void 0);
|
|
4108
4135
|
},
|
|
4109
4136
|
onBuildError: (err2) => sendStatus("error", String(err2)),
|
|
4110
4137
|
// Watcher died mid-session (EMFILE, permission loss, …): non-fatal, so 'ready' stays but `watcher: 'dead'` flags that saves no longer auto-rebuild.
|
|
@@ -4631,7 +4658,10 @@ var WorkbenchSettingsSaveSchema = z3.tuple([
|
|
|
4631
4658
|
port: z3.number().int().min(0).max(65535)
|
|
4632
4659
|
}),
|
|
4633
4660
|
compile: z3.looseObject({
|
|
4634
|
-
|
|
4661
|
+
autoBuild: z3.boolean()
|
|
4662
|
+
}),
|
|
4663
|
+
preview: z3.looseObject({
|
|
4664
|
+
autoReload: z3.boolean()
|
|
4635
4665
|
}),
|
|
4636
4666
|
theme: z3.enum(["system", "light", "dark"]),
|
|
4637
4667
|
lastCreateBaseDir: z3.union([z3.string(), z3.null()])
|
|
@@ -5298,7 +5328,7 @@ var ServiceHostPool = class {
|
|
|
5298
5328
|
/**
|
|
5299
5329
|
* Hand a ready window matching `spec` to the caller. If none is ready, fall
|
|
5300
5330
|
* back to synchronously constructing a fresh window — never blocking on warm
|
|
5301
|
-
* (
|
|
5331
|
+
* (see prewarm-webview.md's invariants section). Fallback windows carry `entryId === null` and
|
|
5302
5332
|
* are destroyed (not pooled) on release.
|
|
5303
5333
|
*
|
|
5304
5334
|
* A spec change (different `preloadPath`) tears down all *pooled* entries and
|
|
@@ -5374,7 +5404,7 @@ var ServiceHostPool = class {
|
|
|
5374
5404
|
}
|
|
5375
5405
|
/**
|
|
5376
5406
|
* Re-target the pool. `target` is clamped to `[0, maxPoolSize]`. Pooled
|
|
5377
|
-
* entries beyond the target are disposed OLDEST-FIRST (
|
|
5407
|
+
* entries beyond the target are disposed OLDEST-FIRST (see prewarm-webview.md); in-use
|
|
5378
5408
|
* entries are untouched. Does not warm new entries up (use `init` / refill).
|
|
5379
5409
|
*/
|
|
5380
5410
|
resize(target) {
|
|
@@ -5503,7 +5533,7 @@ var ServiceHostPool = class {
|
|
|
5503
5533
|
}
|
|
5504
5534
|
/**
|
|
5505
5535
|
* Reset a released window back to a clean, blank state. Order matters
|
|
5506
|
-
* (
|
|
5536
|
+
* (see prewarm-webview.md's state-machine and reset-ordering sections): navigate to blank FIRST so the old document
|
|
5507
5537
|
* stops running and in-flight requests are aborted, THEN clear storage — so a
|
|
5508
5538
|
* still-live old page cannot re-populate storage after the clear.
|
|
5509
5539
|
*/
|
|
@@ -11044,8 +11074,12 @@ async function fsStat(ctx) {
|
|
|
11044
11074
|
}
|
|
11045
11075
|
async function fsReaddir(ctx) {
|
|
11046
11076
|
const entries = await readdirWithin(ctx.projectRoot, ctx.rel);
|
|
11047
|
-
const visible = entries.filter((e) => !(e.isDirectory
|
|
11048
|
-
jsonRes(
|
|
11077
|
+
const visible = entries.filter((e) => !(e.isDirectory && SKIP_DIRS.has(e.name)));
|
|
11078
|
+
jsonRes(
|
|
11079
|
+
ctx.res,
|
|
11080
|
+
200,
|
|
11081
|
+
visible.map((e) => e.isDirectory ? [e.name, 2] : [e.name, 1, e.size, e.mtimeMs])
|
|
11082
|
+
);
|
|
11049
11083
|
}
|
|
11050
11084
|
async function fsRead(ctx) {
|
|
11051
11085
|
const buf = await readFileBufferWithin(ctx.projectRoot, ctx.rel);
|
|
@@ -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
|
|
@@ -2,7 +2,13 @@ import type { WorkbenchContext } from '../services/workbench-context.js';
|
|
|
2
2
|
import type { Disposable } from '@dimina-kit/electron-deck/main';
|
|
3
3
|
/** Dependency/VCS/build directories invisible to every project-tree consumer:
|
|
4
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.
|
|
5
|
+
* WAL ledger seed never walk into them), and the `/__fs/watch` change stream.
|
|
6
|
+
* Derived from devkit's `WATCH_IGNORE_DIRS` (the shared never-source core:
|
|
7
|
+
* node_modules + VCS) so the `node_modules` omission that once wedged
|
|
8
|
+
* `watcher.close()` can never drift back on this side, PLUS the editor mirror's
|
|
9
|
+
* own build-output names (`dist`/`build`/tool caches) — which the devkit
|
|
10
|
+
* recompile watcher deliberately does NOT ignore, since app.json may declare
|
|
11
|
+
* pages under such paths. */
|
|
6
12
|
export declare const SKIP_DIRS: ReadonlySet<string>;
|
|
7
13
|
/**
|
|
8
14
|
* Synchronous, string-level guard: reject empty root (ENOACTIVE), NUL bytes
|
|
@@ -79,8 +85,31 @@ export declare function writeFile(root: string, p: string, content: string | Buf
|
|
|
79
85
|
* out-of-sandbox metadata. `rel` is a project-root-relative bridge path.
|
|
80
86
|
*/
|
|
81
87
|
export declare function statWithin(root: string, rel: string): Promise<import('node:fs').Stats>;
|
|
82
|
-
/**
|
|
83
|
-
|
|
88
|
+
/**
|
|
89
|
+
* One `readdirWithin` result entry. Directories carry no stat (the caller —
|
|
90
|
+
* the `/__fs/readdir` bridge handler — never needs it: a directory recurses
|
|
91
|
+
* instead of being compared). `size`/`mtimeMs` are omitted (not `null`) when
|
|
92
|
+
* the entry raced a concurrent delete between `readdir` and `stat` — the
|
|
93
|
+
* bridge wire format then drops them too, which the sync engine's watch
|
|
94
|
+
* expansion (dimina-kit workbench's wal-audit-watch-expand.ts) treats as
|
|
95
|
+
* "always changed" rather than risking a false stat match.
|
|
96
|
+
*/
|
|
97
|
+
export interface ReaddirStatEntry {
|
|
98
|
+
name: string;
|
|
99
|
+
isDirectory: boolean;
|
|
100
|
+
size?: number;
|
|
101
|
+
mtimeMs?: number;
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Realpath-contained `fs.readdir` twin (see {@link statWithin}) that also
|
|
105
|
+
* `fs.stat`s every FILE entry (size + mtimeMs) — the disk-side half of the
|
|
106
|
+
* sync engine's stat-diffing (see wal-audit-watch-expand.ts's module doc):
|
|
107
|
+
* a watch-reported directory is only worth a full content re-read when a
|
|
108
|
+
* file's stat inside it actually moved, and the `/__fs/readdir` bridge is
|
|
109
|
+
* the one HTTP round trip cheap enough to do that check on every watch
|
|
110
|
+
* event without itself re-reading file bytes.
|
|
111
|
+
*/
|
|
112
|
+
export declare function readdirWithin(root: string, rel: string): Promise<ReaddirStatEntry[]>;
|
|
84
113
|
/**
|
|
85
114
|
* Raw-byte read twin of {@link readFileBuffer} taking a project-root-relative
|
|
86
115
|
* bridge path (the COI `/__fs/read` bridge), with the same realpath +
|
|
@@ -47,14 +47,21 @@ import { z } from 'zod';
|
|
|
47
47
|
import { IpcRegistry } from '../utils/ipc-registry.js';
|
|
48
48
|
import { validate } from '../utils/ipc-schema.js';
|
|
49
49
|
import { ProjectFsChannel } from '../../shared/ipc-channels.js';
|
|
50
|
+
import { WATCH_IGNORE_DIRS } from '@dimina-kit/devkit/watch-ignore';
|
|
50
51
|
/** Cap for `listFiles` — huge monorepos would otherwise stall the renderer. */
|
|
51
52
|
const LIST_FILE_LIMIT = 5000;
|
|
52
53
|
/** Dependency/VCS/build directories invisible to every project-tree consumer:
|
|
53
54
|
* 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
|
+
* WAL ledger seed never walk into them), and the `/__fs/watch` change stream.
|
|
56
|
+
* Derived from devkit's `WATCH_IGNORE_DIRS` (the shared never-source core:
|
|
57
|
+
* node_modules + VCS) so the `node_modules` omission that once wedged
|
|
58
|
+
* `watcher.close()` can never drift back on this side, PLUS the editor mirror's
|
|
59
|
+
* own build-output names (`dist`/`build`/tool caches) — which the devkit
|
|
60
|
+
* recompile watcher deliberately does NOT ignore, since app.json may declare
|
|
61
|
+
* pages under such paths. */
|
|
55
62
|
export const SKIP_DIRS = new Set([
|
|
56
|
-
|
|
57
|
-
'.next', '.cache', '.turbo', '.parcel-cache', '.nuxt', '.output', '.vite',
|
|
63
|
+
...WATCH_IGNORE_DIRS,
|
|
64
|
+
'dist', 'build', '.next', '.cache', '.turbo', '.parcel-cache', '.nuxt', '.output', '.vite',
|
|
58
65
|
]);
|
|
59
66
|
const PathArg = z.string().min(1);
|
|
60
67
|
const ReadFileSchema = z.tuple([PathArg]);
|
|
@@ -319,10 +326,31 @@ export async function statWithin(root, rel) {
|
|
|
319
326
|
const safe = await resolveWithinProjectRoot(joinRel(root, rel), root);
|
|
320
327
|
return fs.stat(safe);
|
|
321
328
|
}
|
|
322
|
-
/**
|
|
329
|
+
/**
|
|
330
|
+
* Realpath-contained `fs.readdir` twin (see {@link statWithin}) that also
|
|
331
|
+
* `fs.stat`s every FILE entry (size + mtimeMs) — the disk-side half of the
|
|
332
|
+
* sync engine's stat-diffing (see wal-audit-watch-expand.ts's module doc):
|
|
333
|
+
* a watch-reported directory is only worth a full content re-read when a
|
|
334
|
+
* file's stat inside it actually moved, and the `/__fs/readdir` bridge is
|
|
335
|
+
* the one HTTP round trip cheap enough to do that check on every watch
|
|
336
|
+
* event without itself re-reading file bytes.
|
|
337
|
+
*/
|
|
323
338
|
export async function readdirWithin(root, rel) {
|
|
324
339
|
const safe = await resolveWithinProjectRoot(joinRel(root, rel), root);
|
|
325
|
-
|
|
340
|
+
const dirents = await fs.readdir(safe, { withFileTypes: true });
|
|
341
|
+
return Promise.all(dirents.map(async (d) => {
|
|
342
|
+
if (d.isDirectory())
|
|
343
|
+
return { name: d.name, isDirectory: true };
|
|
344
|
+
try {
|
|
345
|
+
const st = await fs.stat(path.join(safe, d.name));
|
|
346
|
+
return { name: d.name, isDirectory: false, size: st.size, mtimeMs: st.mtimeMs };
|
|
347
|
+
}
|
|
348
|
+
catch {
|
|
349
|
+
// Entry vanished (or became unreadable) between readdir and stat —
|
|
350
|
+
// report it stat-less; see this function's doc for why that is safe.
|
|
351
|
+
return { name: d.name, isDirectory: false };
|
|
352
|
+
}
|
|
353
|
+
}));
|
|
326
354
|
}
|
|
327
355
|
/**
|
|
328
356
|
* Raw-byte read twin of {@link readFileBuffer} taking a project-root-relative
|
|
@@ -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
|
|
@@ -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);
|
|
@@ -94,7 +94,7 @@ export interface ServiceHostPoolInitOptions {
|
|
|
94
94
|
defaultPoolSize: number;
|
|
95
95
|
/** Spec used to warm the initial entries. */
|
|
96
96
|
defaultSpec: ServiceHostSpec;
|
|
97
|
-
/** Hard ceiling on pooled entries. Clamped to ≤ 4 (
|
|
97
|
+
/** Hard ceiling on pooled entries. Clamped to ≤ 4 (see prewarm-webview.md). Default 3. */
|
|
98
98
|
maxPoolSize?: number;
|
|
99
99
|
}
|
|
100
100
|
export declare class ServiceHostPool {
|
|
@@ -116,7 +116,7 @@ export declare class ServiceHostPool {
|
|
|
116
116
|
/**
|
|
117
117
|
* Hand a ready window matching `spec` to the caller. If none is ready, fall
|
|
118
118
|
* back to synchronously constructing a fresh window — never blocking on warm
|
|
119
|
-
* (
|
|
119
|
+
* (see prewarm-webview.md's invariants section). Fallback windows carry `entryId === null` and
|
|
120
120
|
* are destroyed (not pooled) on release.
|
|
121
121
|
*
|
|
122
122
|
* A spec change (different `preloadPath`) tears down all *pooled* entries and
|
|
@@ -144,7 +144,7 @@ export declare class ServiceHostPool {
|
|
|
144
144
|
releaseDestroyed(entryId: string | null): void;
|
|
145
145
|
/**
|
|
146
146
|
* Re-target the pool. `target` is clamped to `[0, maxPoolSize]`. Pooled
|
|
147
|
-
* entries beyond the target are disposed OLDEST-FIRST (
|
|
147
|
+
* entries beyond the target are disposed OLDEST-FIRST (see prewarm-webview.md); in-use
|
|
148
148
|
* entries are untouched. Does not warm new entries up (use `init` / refill).
|
|
149
149
|
*/
|
|
150
150
|
resize(target: number): void;
|
|
@@ -170,7 +170,7 @@ export declare class ServiceHostPool {
|
|
|
170
170
|
private warmOne;
|
|
171
171
|
/**
|
|
172
172
|
* Reset a released window back to a clean, blank state. Order matters
|
|
173
|
-
* (
|
|
173
|
+
* (see prewarm-webview.md's state-machine and reset-ordering sections): navigate to blank FIRST so the old document
|
|
174
174
|
* stops running and in-flight requests are aborted, THEN clear storage — so a
|
|
175
175
|
* still-live old page cannot re-populate storage after the clear.
|
|
176
176
|
*/
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { BrowserWindow } from 'electron';
|
|
2
2
|
/** Blank placeholder URL loaded while warming / after reset. */
|
|
3
3
|
const BLANK_URL = 'about:blank';
|
|
4
|
-
/** Hard ceiling on pool size regardless of requested size (
|
|
4
|
+
/** Hard ceiling on pool size regardless of requested size (see prewarm-webview.md). */
|
|
5
5
|
const HARD_MAX_POOL_SIZE = 4;
|
|
6
|
-
/** Storage buckets cleared on every release (
|
|
6
|
+
/** Storage buckets cleared on every release (see prewarm-webview.md's reset checklist). */
|
|
7
7
|
const RESET_STORAGES = [
|
|
8
8
|
'cookies',
|
|
9
9
|
'localstorage',
|
|
@@ -37,7 +37,7 @@ export class ServiceHostPool {
|
|
|
37
37
|
/**
|
|
38
38
|
* Hand a ready window matching `spec` to the caller. If none is ready, fall
|
|
39
39
|
* back to synchronously constructing a fresh window — never blocking on warm
|
|
40
|
-
* (
|
|
40
|
+
* (see prewarm-webview.md's invariants section). Fallback windows carry `entryId === null` and
|
|
41
41
|
* are destroyed (not pooled) on release.
|
|
42
42
|
*
|
|
43
43
|
* A spec change (different `preloadPath`) tears down all *pooled* entries and
|
|
@@ -129,7 +129,7 @@ export class ServiceHostPool {
|
|
|
129
129
|
}
|
|
130
130
|
/**
|
|
131
131
|
* Re-target the pool. `target` is clamped to `[0, maxPoolSize]`. Pooled
|
|
132
|
-
* entries beyond the target are disposed OLDEST-FIRST (
|
|
132
|
+
* entries beyond the target are disposed OLDEST-FIRST (see prewarm-webview.md); in-use
|
|
133
133
|
* entries are untouched. Does not warm new entries up (use `init` / refill).
|
|
134
134
|
*/
|
|
135
135
|
resize(target) {
|
|
@@ -275,7 +275,7 @@ export class ServiceHostPool {
|
|
|
275
275
|
}
|
|
276
276
|
/**
|
|
277
277
|
* Reset a released window back to a clean, blank state. Order matters
|
|
278
|
-
* (
|
|
278
|
+
* (see prewarm-webview.md's state-machine and reset-ordering sections): navigate to blank FIRST so the old document
|
|
279
279
|
* stops running and in-flight requests are aborted, THEN clear storage — so a
|
|
280
280
|
* still-live old page cannot re-populate storage after the clear.
|
|
281
281
|
*/
|
|
@@ -10,7 +10,13 @@ export interface WorkbenchSettings {
|
|
|
10
10
|
};
|
|
11
11
|
compile: {
|
|
12
12
|
/** Watch project files and auto-recompile on change. */
|
|
13
|
-
|
|
13
|
+
autoBuild: boolean;
|
|
14
|
+
};
|
|
15
|
+
preview: {
|
|
16
|
+
/** Reload the simulator once a watcher-triggered rebuild lands. Independent
|
|
17
|
+
* of `compile.autoBuild`: "auto-compile on save, reload manually" is valid
|
|
18
|
+
* and preserves the running page stack / form state across a save. */
|
|
19
|
+
autoReload: boolean;
|
|
14
20
|
};
|
|
15
21
|
theme: ThemeSource;
|
|
16
22
|
/**
|
|
@@ -12,7 +12,10 @@ const DEFAULTS = {
|
|
|
12
12
|
port: 7789,
|
|
13
13
|
},
|
|
14
14
|
compile: {
|
|
15
|
-
|
|
15
|
+
autoBuild: true,
|
|
16
|
+
},
|
|
17
|
+
preview: {
|
|
18
|
+
autoReload: true,
|
|
16
19
|
},
|
|
17
20
|
theme: 'system',
|
|
18
21
|
lastCreateBaseDir: null,
|
|
@@ -20,6 +23,13 @@ const DEFAULTS = {
|
|
|
20
23
|
function getSettingsFile() {
|
|
21
24
|
return path.join(app.getPath('userData'), 'dimina-workbench-settings.json');
|
|
22
25
|
}
|
|
26
|
+
/** Accept `value` only when it is a real boolean, else `fallback`. A bare `??`
|
|
27
|
+
* chain guards only null/undefined, so a hand-edited or corrupt config with a
|
|
28
|
+
* stringy `"false"` would pass through and read as truthy downstream — silently
|
|
29
|
+
* keeping a toggle ON when the user meant OFF. */
|
|
30
|
+
function pickBool(value, fallback) {
|
|
31
|
+
return typeof value === 'boolean' ? value : fallback;
|
|
32
|
+
}
|
|
23
33
|
export function loadWorkbenchSettings() {
|
|
24
34
|
try {
|
|
25
35
|
const data = JSON.parse(fs.readFileSync(getSettingsFile(), 'utf-8'));
|
|
@@ -36,7 +46,16 @@ export function loadWorkbenchSettings() {
|
|
|
36
46
|
port: data.mcp?.port ?? DEFAULTS.mcp.port,
|
|
37
47
|
},
|
|
38
48
|
compile: {
|
|
39
|
-
|
|
49
|
+
// Legacy files persisted a single `compile.watch`; the new schema
|
|
50
|
+
// splits it into `compile.autoBuild` (recompile on save) and
|
|
51
|
+
// `preview.autoReload` (reload the simulator afterwards). Prefer the new
|
|
52
|
+
// key, fall back to the legacy one so an old settings file keeps its
|
|
53
|
+
// "auto-compile" choice; `preview.autoReload` defaults to true (the old
|
|
54
|
+
// always-reload behavior) when the file predates the split.
|
|
55
|
+
autoBuild: pickBool(data.compile?.autoBuild, pickBool(data.compile?.watch, DEFAULTS.compile.autoBuild)),
|
|
56
|
+
},
|
|
57
|
+
preview: {
|
|
58
|
+
autoReload: pickBool(data.preview?.autoReload, DEFAULTS.preview.autoReload),
|
|
40
59
|
},
|
|
41
60
|
theme,
|
|
42
61
|
lastCreateBaseDir: typeof data.lastCreateBaseDir === 'string'
|
|
@@ -50,6 +69,7 @@ export function loadWorkbenchSettings() {
|
|
|
50
69
|
cdp: { ...DEFAULTS.cdp },
|
|
51
70
|
mcp: { ...DEFAULTS.mcp },
|
|
52
71
|
compile: { ...DEFAULTS.compile },
|
|
72
|
+
preview: { ...DEFAULTS.preview },
|
|
53
73
|
};
|
|
54
74
|
}
|
|
55
75
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Main-process disk reader/writer backing `difile://_store/*` and
|
|
3
3
|
* `difile://<usr-rel>` URLs.
|
|
4
4
|
*
|
|
5
|
-
* Spec: `packages/devtools/docs/file-system.md
|
|
5
|
+
* Spec: `packages/devtools/docs/file-system.md`.
|
|
6
6
|
*
|
|
7
7
|
* - `readDiskFile(realPath, opts?)`: read a (possibly ranged) slice of a
|
|
8
8
|
* file inside the USER_DATA_PATH sandbox. Returns `bytes` plus the
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Main-process disk reader/writer backing `difile://_store/*` and
|
|
3
3
|
* `difile://<usr-rel>` URLs.
|
|
4
4
|
*
|
|
5
|
-
* Spec: `packages/devtools/docs/file-system.md
|
|
5
|
+
* Spec: `packages/devtools/docs/file-system.md`.
|
|
6
6
|
*
|
|
7
7
|
* - `readDiskFile(realPath, opts?)`: read a (possibly ranged) slice of a
|
|
8
8
|
* file inside the USER_DATA_PATH sandbox. Returns `bytes` plus the
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure dispatcher for `difile://` URL requests.
|
|
3
3
|
*
|
|
4
|
-
* Spec: `packages/devtools/docs/file-system.md
|
|
4
|
+
* Spec: `packages/devtools/docs/file-system.md`.
|
|
5
5
|
*
|
|
6
6
|
* The shipping implementation in `index.ts` registers a thin
|
|
7
7
|
* `simSession.protocol.handle('difile')` wrapper that delegates here; the race
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pure dispatcher for `difile://` URL requests.
|
|
3
3
|
*
|
|
4
|
-
* Spec: `packages/devtools/docs/file-system.md
|
|
4
|
+
* Spec: `packages/devtools/docs/file-system.md`.
|
|
5
5
|
*
|
|
6
6
|
* The shipping implementation in `index.ts` registers a thin
|
|
7
7
|
* `simSession.protocol.handle('difile')` wrapper that delegates here; the race
|
|
@@ -206,8 +206,15 @@ async function fsReaddir(ctx) {
|
|
|
206
206
|
// every level: the workbench mirror and the WAL ledger seed walk whatever
|
|
207
207
|
// this returns, so listing them would pull entire dependency trees into the
|
|
208
208
|
// editor memfs and the OPFS ledger. Same-named FILES stay visible.
|
|
209
|
-
const visible = entries.filter((e) => !(e.isDirectory
|
|
210
|
-
|
|
209
|
+
const visible = entries.filter((e) => !(e.isDirectory && SKIP_DIRS.has(e.name)));
|
|
210
|
+
// Wire shape: `[name, type]` for a directory, `[name, type, size, mtimeMs]`
|
|
211
|
+
// for a file — the sync engine's watch-batch stat-diffing (workbench's
|
|
212
|
+
// wal-audit-watch-expand.ts) needs size+mtimeMs to tell a stat-unchanged
|
|
213
|
+
// survivor from an actually-modified file without re-reading its content.
|
|
214
|
+
// `size`/`mtimeMs` are simply absent (not `null`) for a stat-less entry
|
|
215
|
+
// (readdirWithin's readdir/stat race) — the client side then always
|
|
216
|
+
// treats it as changed.
|
|
217
|
+
jsonRes(ctx.res, 200, visible.map((e) => (e.isDirectory ? [e.name, 2] : [e.name, 1, e.size, e.mtimeMs])));
|
|
211
218
|
}
|
|
212
219
|
async function fsRead(ctx) {
|
|
213
220
|
const buf = await readFileBufferWithin(ctx.projectRoot, ctx.rel);
|