@dimina-kit/devkit 0.1.2-dev.20260702182435 → 0.1.2-dev.20260703101348

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/index.d.ts CHANGED
@@ -41,6 +41,14 @@ export interface OpenProjectOptions {
41
41
  * worker's piped stdout/stderr, tagged with their source stream.
42
42
  */
43
43
  onLog?: (entry: CompileLogEntry) => void;
44
+ /**
45
+ * Fired when the file watcher emits `'error'` (EMFILE, permission loss, …)
46
+ * AFTER its initial scan already resolved `ready` — i.e. the session is
47
+ * live and auto-rebuild-on-save has silently stopped working. A pre-ready
48
+ * error instead rejects `openProject` itself (see `createProjectWatcher`),
49
+ * so this only ever fires once per session, at most.
50
+ */
51
+ onWatcherError?: (err: unknown) => void;
44
52
  }
45
53
  export declare function openProject(opts: OpenProjectOptions): Promise<ProjectSession>;
46
54
  /**
@@ -49,7 +57,7 @@ export declare function openProject(opts: OpenProjectOptions): Promise<ProjectSe
49
57
  * watcher and whose `ready` resolves once the initial scan is complete (only
50
58
  * changes made after that point are guaranteed to surface).
51
59
  */
52
- export declare function createProjectWatcher(projectPath: string, onChange: () => void | Promise<void>): {
60
+ export declare function createProjectWatcher(projectPath: string, onChange: () => void | Promise<void>, onWatcherError?: (err: unknown) => void): {
53
61
  close: () => Promise<void>;
54
62
  ready: Promise<void>;
55
63
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAA;AAE1D,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAyCzE,MAAM,WAAW,OAAO;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oFAAoF;IACpF,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;CACxC;AAED,wBAAsB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAgLnF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAClC;IAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CA+CtD"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,EAAgB,eAAe,EAAiB,MAAM,qBAAqB,CAAA;AAEvF,OAAO,EAAE,sBAAsB,EAAE,MAAM,wBAAwB,CAAA;AAC/D,YAAY,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAA;AAC9D,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAA;AACpD,OAAO,EAAE,mBAAmB,EAAE,MAAM,qBAAqB,CAAA;AACzD,YAAY,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA;AAyCzE,MAAM,WAAW,OAAO;IACvB,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,cAAc;IAC9B,OAAO,EAAE,OAAO,CAAA;IAChB,IAAI,EAAE,MAAM,CAAA;IACZ,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED,MAAM,WAAW,kBAAkB;IAClC,WAAW,EAAE,MAAM,CAAA;IACnB,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,SAAS,CAAC,EAAE,OAAO,CAAA;IACnB;;;;OAIG;IACH,SAAS,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QAAC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAA;KAAE,CAAA;IAC5E,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,YAAY,CAAC,EAAE,MAAM,CAAA;IACrB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,oFAAoF;IACpF,KAAK,CAAC,EAAE,OAAO,CAAA;IACf,SAAS,CAAC,EAAE,MAAM,IAAI,CAAA;IACtB,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;IACrC;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC;;;;;;OAMG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,CAAA;CACvC;AAuFD,wBAAsB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAwInF;AAED;;;;;GAKG;AACH,wBAAgB,oBAAoB,CACnC,WAAW,EAAE,MAAM,EACnB,QAAQ,EAAE,MAAM,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,EACpC,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,IAAI,GACrC;IAAE,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,KAAK,EAAE,OAAO,CAAC,IAAI,CAAC,CAAA;CAAE,CA2DtD"}
package/dist/index.js CHANGED
@@ -38,8 +38,83 @@ function getRandomPort() {
38
38
  srv.on('error', reject);
39
39
  });
40
40
  }
41
+ /**
42
+ * Best-effort fallback when the compiler returns no AppInfo (e.g. a build
43
+ * racing against a just-closed project in the same Electron process): reads
44
+ * the canonical appid from project.config.json so storage prefixing (which
45
+ * keys off appInfo.appId) still lines up with the runtime instead of
46
+ * silently falling to 'unknown'.
47
+ */
48
+ function resolveAppInfoFallback(projectPath) {
49
+ try {
50
+ const configRaw = fs.readFileSync(path.join(projectPath, 'project.config.json'), 'utf8');
51
+ const config = JSON.parse(configRaw);
52
+ if (!config.appid || typeof config.appid !== 'string' || config.appid.length === 0)
53
+ return null;
54
+ return {
55
+ appId: config.appid,
56
+ name: config.projectname ?? path.basename(projectPath),
57
+ path: projectPath,
58
+ };
59
+ }
60
+ catch {
61
+ // best-effort — fall through to the 'unknown' fallback in the caller
62
+ return null;
63
+ }
64
+ }
65
+ /** Insert or replace `rebuilt` in the session's app list, keyed by appId. */
66
+ function upsertSessionApp(sessionApps, rebuilt) {
67
+ const idx = sessionApps.findIndex(a => a.appId === rebuilt.appId);
68
+ if (idx === -1)
69
+ sessionApps.push(rebuilt);
70
+ else
71
+ sessionApps[idx] = rebuilt;
72
+ }
73
+ /**
74
+ * Run one watcher-triggered rebuild. `getReload` is read at call time (not
75
+ * captured eagerly) because `reload` is only assigned once the dev server has
76
+ * started, after this runner is wired into the rebuild scheduler.
77
+ */
78
+ async function runRebuild(compileWorker, buildRequest, sessionApps, getReload, onRebuild, onBuildError) {
79
+ try {
80
+ const rebuilt = (await compileWorker.build(buildRequest));
81
+ if (rebuilt)
82
+ upsertSessionApp(sessionApps, rebuilt);
83
+ getReload()?.();
84
+ onRebuild?.();
85
+ }
86
+ catch (e) {
87
+ onBuildError?.(e);
88
+ }
89
+ }
90
+ /**
91
+ * Tear down whatever partially started before `openProject` failed: the
92
+ * watcher, the forked compile worker, and the dev server if it was already
93
+ * listening. Every step is best-effort so a secondary failure here never
94
+ * masks the primary error the caller is about to rethrow.
95
+ */
96
+ async function cleanupFailedOpen(watcher, compileWorker, server) {
97
+ try {
98
+ await watcher?.close();
99
+ }
100
+ catch {
101
+ // best-effort — the open is already failing with the primary error
102
+ }
103
+ try {
104
+ await compileWorker.close();
105
+ }
106
+ catch {
107
+ // best-effort — never mask the primary error
108
+ }
109
+ if (server) {
110
+ ;
111
+ server.closeAllConnections?.();
112
+ const listening = server;
113
+ await new Promise(resolve => listening.close(() => resolve()));
114
+ }
115
+ }
41
116
  export async function openProject(opts) {
42
- const { projectPath: rawProjectPath, port = 0, sourcemap = false, fileTypes, simulatorDir, containerDir: overrideContainerDir, outputDir, watch = true, onRebuild, onBuildError, onLog, } = opts;
117
+ const { projectPath: rawProjectPath, port = 0, sourcemap = false, fileTypes, simulatorDir, containerDir: overrideContainerDir, outputDir, watch = true, onRebuild, onBuildError, onLog, onWatcherError, } = opts;
43
118
  const projectPath = path.resolve(rawProjectPath);
44
119
  const buildOptions = { sourcemap, fileTypes };
45
120
  const resolvedPort = port === 0 ? await getRandomPort() : port;
@@ -78,20 +153,7 @@ export async function openProject(opts) {
78
153
  // `'unknown'` is only used as a last resort when the manifest itself
79
154
  // is missing/unreadable, preserving the original error path.
80
155
  if (!initialAppInfo) {
81
- try {
82
- const configRaw = fs.readFileSync(path.join(projectPath, 'project.config.json'), 'utf8');
83
- const config = JSON.parse(configRaw);
84
- if (config.appid && typeof config.appid === 'string' && config.appid.length > 0) {
85
- initialAppInfo = {
86
- appId: config.appid,
87
- name: config.projectname ?? path.basename(projectPath),
88
- path: projectPath,
89
- };
90
- }
91
- }
92
- catch {
93
- // best-effort — fall through to the 'unknown' fallback below
94
- }
156
+ initialAppInfo = resolveAppInfoFallback(projectPath);
95
157
  }
96
158
  if (!initialAppInfo) {
97
159
  // Both the compile (null AppInfo) and the project.config.json fallback
@@ -114,23 +176,6 @@ export async function openProject(opts) {
114
176
  let server = null;
115
177
  let reload;
116
178
  let watcher = null;
117
- async function rebuild() {
118
- try {
119
- const rebuilt = (await compileWorker.build(buildRequest));
120
- if (rebuilt) {
121
- const idx = sessionApps.findIndex(a => a.appId === rebuilt.appId);
122
- if (idx === -1)
123
- sessionApps.push(rebuilt);
124
- else
125
- sessionApps[idx] = rebuilt;
126
- }
127
- reload?.();
128
- onRebuild?.();
129
- }
130
- catch (e) {
131
- onBuildError?.(e);
132
- }
133
- }
134
179
  try {
135
180
  process.env.DIMINA_NO_OPEN_BROWSER = '1';
136
181
  const fe = await import('../fe/index.js');
@@ -148,8 +193,10 @@ export async function openProject(opts) {
148
193
  // Watcher events are routed through the scheduler so a save landing while
149
194
  // a build is in flight is never dropped: it coalesces into exactly one
150
195
  // trailing rebuild once the current run settles.
151
- const rebuildScheduler = createRebuildScheduler(rebuild);
152
- watcher = watch ? createProjectWatcher(projectPath, () => rebuildScheduler.schedule()) : null;
196
+ const rebuildScheduler = createRebuildScheduler(() => runRebuild(compileWorker, buildRequest, sessionApps, () => reload, onRebuild, onBuildError));
197
+ watcher = watch
198
+ ? createProjectWatcher(projectPath, () => rebuildScheduler.schedule(), onWatcherError)
199
+ : null;
153
200
  // Don't resolve until the watcher's initial scan is done: a save landing
154
201
  // in the gap between `openProject` resolving and chokidar going live
155
202
  // (fsevents stream not yet active on macOS) would otherwise be silently
@@ -159,24 +206,7 @@ export async function openProject(opts) {
159
206
  await watcher?.ready;
160
207
  }
161
208
  catch (err) {
162
- try {
163
- await watcher?.close();
164
- }
165
- catch {
166
- // best-effort — the open is already failing with the primary error
167
- }
168
- try {
169
- await compileWorker.close();
170
- }
171
- catch {
172
- // best-effort — never mask the primary error
173
- }
174
- if (server) {
175
- ;
176
- server.closeAllConnections?.();
177
- const listening = server;
178
- await new Promise(resolve => listening.close(() => resolve()));
179
- }
209
+ await cleanupFailedOpen(watcher, compileWorker, server);
180
210
  throw err;
181
211
  }
182
212
  // `server` is always assigned when the try block completes without throwing
@@ -204,7 +234,7 @@ export async function openProject(opts) {
204
234
  * watcher and whose `ready` resolves once the initial scan is complete (only
205
235
  * changes made after that point are guaranteed to surface).
206
236
  */
207
- export function createProjectWatcher(projectPath, onChange) {
237
+ export function createProjectWatcher(projectPath, onChange, onWatcherError) {
208
238
  // Ignore dotfiles/dot-directories that live *inside* the project (.git,
209
239
  // .DS_Store, editor scratch, etc.) without nuking the whole watch when the
210
240
  // project itself sits under a dotted ancestor path. A regex like
@@ -239,12 +269,24 @@ export function createProjectWatcher(projectPath, onChange) {
239
269
  // A watcher 'error' before the initial scan completes (EMFILE, permission
240
270
  // loss, …) must REJECT `ready`: resolving only on 'ready' would leave
241
271
  // `await watcher.ready` — and the whole openProject — hung forever. The
242
- // listener also doubles as the EventEmitter unhandled-'error' guard. A
243
- // post-ready 'error' makes the reject a settled-promise no-op.
272
+ // listener also doubles as the EventEmitter unhandled-'error' guard. Once
273
+ // `ready` has settled, the reject above is a no-op — that's the live-session
274
+ // case (watcher died mid-session, not at open time) and is instead surfaced
275
+ // through `onWatcherError`, the only remaining signal that auto-rebuild has
276
+ // silently stopped.
277
+ let readySettled = false;
244
278
  const ready = new Promise((resolve, reject) => {
245
- watcher.once('ready', () => resolve());
279
+ watcher.once('ready', () => {
280
+ readySettled = true;
281
+ resolve();
282
+ });
246
283
  watcher.on('error', (err) => {
247
- reject(err instanceof Error ? err : new Error(String(err)));
284
+ if (!readySettled) {
285
+ readySettled = true;
286
+ reject(err instanceof Error ? err : new Error(String(err)));
287
+ return;
288
+ }
289
+ onWatcherError?.(err);
248
290
  });
249
291
  });
250
292
  return {
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=open-project-watcher-error-callback.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"open-project-watcher-error-callback.test.d.ts","sourceRoot":"","sources":["../src/open-project-watcher-error-callback.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,191 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import fs from 'node:fs';
3
+ import os from 'node:os';
4
+ import path from 'node:path';
5
+ import { PassThrough } from 'node:stream';
6
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
7
+ import * as devkit from './index.js';
8
+ /**
9
+ * Contract: `openProject` gains an `onWatcherError?: (err: unknown) => void`
10
+ * option. A watcher 'error' BEFORE the initial scan completes must keep
11
+ * rejecting `openProject` exactly as today (see open-project-cleanup.test.ts)
12
+ * — `onWatcherError` must NOT be called for that pre-ready path, so callers
13
+ * don't get a double signal for the same failure.
14
+ *
15
+ * A watcher 'error' AFTER 'ready' (EMFILE, permission loss mid-session, …) is
16
+ * TODAY silently dropped: `createProjectWatcher`'s `watcher.on('error', ...)`
17
+ * only rejects the (long-since-settled) `ready` promise, a no-op once ready
18
+ * has already resolved. The project keeps "running" with a dead watcher and
19
+ * nothing downstream ever finds out — a save after that point compiles
20
+ * nothing, forever, with no diagnostic. The fix: `openProject` must forward a
21
+ * post-ready watcher 'error' to `onWatcherError` so a caller (devtools'
22
+ * workspace-service) can surface "auto-rebuild stopped working" instead of
23
+ * silently going stale.
24
+ *
25
+ * Harness: verbatim copy of open-project-cleanup.test.ts's fake-fork +
26
+ * fake-watcher setup (fork/fe/chokidar all mocked so the failure point is
27
+ * injectable), plus a FakeWatcher that can fire 'error' AFTER 'ready'.
28
+ */
29
+ const mocks = vi.hoisted(() => ({
30
+ fork: vi.fn(),
31
+ feStart: vi.fn(),
32
+ watch: vi.fn(),
33
+ }));
34
+ vi.mock('node:child_process', async (importOriginal) => {
35
+ const actual = await importOriginal();
36
+ return { ...actual, fork: mocks.fork, default: { ...actual, fork: mocks.fork } };
37
+ });
38
+ vi.mock('child_process', async (importOriginal) => {
39
+ const actual = await importOriginal();
40
+ return { ...actual, fork: mocks.fork, default: { ...actual, fork: mocks.fork } };
41
+ });
42
+ vi.mock('../fe/index.js', () => ({ start: mocks.feStart }));
43
+ vi.mock('chokidar', () => ({
44
+ default: { watch: mocks.watch },
45
+ watch: mocks.watch,
46
+ }));
47
+ class FakeChild extends EventEmitter {
48
+ stdout = new PassThrough();
49
+ stderr = new PassThrough();
50
+ connected = true;
51
+ pid = 4343;
52
+ send = vi.fn((msg) => {
53
+ const m = msg;
54
+ if (m && m.cmd === 'build') {
55
+ queueMicrotask(() => {
56
+ if (!this.connected)
57
+ return;
58
+ this.emit('message', {
59
+ type: 'result',
60
+ appInfo: { appId: 'watcher_err_app_001', name: 'watcher-err-app', path: String(m.projectPath ?? '') },
61
+ });
62
+ });
63
+ }
64
+ return true;
65
+ });
66
+ kill = vi.fn(() => {
67
+ this.connected = false;
68
+ queueMicrotask(() => this.emit('exit', null, 'SIGTERM'));
69
+ return true;
70
+ });
71
+ }
72
+ class FakeWatcher extends EventEmitter {
73
+ close = vi.fn(async () => { });
74
+ }
75
+ function makeFakeFe() {
76
+ const server = {
77
+ close: vi.fn((cb) => {
78
+ cb?.();
79
+ return server;
80
+ }),
81
+ closeAllConnections: vi.fn(),
82
+ };
83
+ return { server, reload: vi.fn() };
84
+ }
85
+ const children = [];
86
+ const watchers = [];
87
+ const feInstances = [];
88
+ const cleanupRoots = [];
89
+ beforeEach(() => {
90
+ children.length = 0;
91
+ watchers.length = 0;
92
+ feInstances.length = 0;
93
+ mocks.fork.mockReset();
94
+ mocks.fork.mockImplementation(() => {
95
+ const child = new FakeChild();
96
+ children.push(child);
97
+ return child;
98
+ });
99
+ mocks.feStart.mockReset();
100
+ mocks.feStart.mockImplementation(async () => {
101
+ const fe = makeFakeFe();
102
+ feInstances.push(fe);
103
+ return fe;
104
+ });
105
+ mocks.watch.mockReset();
106
+ });
107
+ afterEach(() => {
108
+ for (const root of cleanupRoots.splice(0)) {
109
+ fs.rmSync(root, { recursive: true, force: true });
110
+ }
111
+ });
112
+ function makeFixture() {
113
+ const root = fs.mkdtempSync(path.join(os.tmpdir(), 'devkit-watcher-err-'));
114
+ cleanupRoots.push(root);
115
+ fs.writeFileSync(path.join(root, 'project.config.json'), JSON.stringify({ appid: 'watcher_err_app_001', projectname: 'watcher-err-app' }));
116
+ return root;
117
+ }
118
+ function sleep(ms) {
119
+ return new Promise(resolve => setTimeout(resolve, ms));
120
+ }
121
+ describe('openProject onWatcherError — post-ready watcher errors are surfaced, pre-ready path is unchanged', () => {
122
+ it('does NOT call onWatcherError for a watcher error BEFORE ready (openProject still rejects, same as today)', async () => {
123
+ const root = makeFixture();
124
+ mocks.watch.mockImplementation(() => {
125
+ const watcher = new FakeWatcher();
126
+ watchers.push(watcher);
127
+ setTimeout(() => {
128
+ try {
129
+ watcher.emit('error', new Error('EMFILE: too many open files, watch'));
130
+ }
131
+ catch {
132
+ // no 'error' listener path — matches open-project-cleanup.test.ts
133
+ }
134
+ }, 10);
135
+ return watcher;
136
+ });
137
+ const onWatcherError = vi.fn();
138
+ await expect(devkit.openProject({
139
+ projectPath: root,
140
+ watch: true,
141
+ outputDir: path.join(root, '.out'),
142
+ onWatcherError,
143
+ })).rejects.toThrow();
144
+ expect(onWatcherError, 'a pre-ready watcher error must keep rejecting openProject WITHOUT also invoking onWatcherError — '
145
+ + 'the caller already gets the failure via the rejected promise').not.toHaveBeenCalled();
146
+ }, 15_000);
147
+ it('calls onWatcherError with the error when the watcher fails AFTER ready — openProject has already resolved by then', async () => {
148
+ const root = makeFixture();
149
+ let capturedWatcher = null;
150
+ mocks.watch.mockImplementation(() => {
151
+ const watcher = new FakeWatcher();
152
+ watchers.push(watcher);
153
+ capturedWatcher = watcher;
154
+ queueMicrotask(() => watcher.emit('ready'));
155
+ return watcher;
156
+ });
157
+ const onWatcherError = vi.fn();
158
+ const session = await devkit.openProject({
159
+ projectPath: root,
160
+ watch: true,
161
+ outputDir: path.join(root, '.out'),
162
+ onWatcherError,
163
+ });
164
+ const postReadyError = new Error('EMFILE: too many open files, watch (post-ready)');
165
+ capturedWatcher.emit('error', postReadyError);
166
+ await sleep(20);
167
+ expect(onWatcherError, 'today nothing listens for a post-ready watcher error beyond the already-settled ready promise — '
168
+ + 'the project silently stops auto-rebuilding forever with no signal').toHaveBeenCalledWith(postReadyError);
169
+ await session.close();
170
+ }, 15_000);
171
+ it('a post-ready watcher error does not crash the process (no unhandled "error" event) even without onWatcherError supplied', async () => {
172
+ const root = makeFixture();
173
+ let capturedWatcher = null;
174
+ mocks.watch.mockImplementation(() => {
175
+ const watcher = new FakeWatcher();
176
+ watchers.push(watcher);
177
+ capturedWatcher = watcher;
178
+ queueMicrotask(() => watcher.emit('ready'));
179
+ return watcher;
180
+ });
181
+ const session = await devkit.openProject({
182
+ projectPath: root,
183
+ watch: true,
184
+ outputDir: path.join(root, '.out'),
185
+ });
186
+ expect(() => {
187
+ capturedWatcher.emit('error', new Error('EMFILE (no onWatcherError supplied)'));
188
+ }).not.toThrow();
189
+ await session.close();
190
+ }, 15_000);
191
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dimina-kit/devkit",
3
- "version": "0.1.2-dev.20260702182435",
3
+ "version": "0.1.2-dev.20260703101348",
4
4
  "description": "Development toolkit for Dimina mini-apps with H5 container preview",
5
5
  "keywords": [
6
6
  "dimina",
@@ -62,7 +62,7 @@
62
62
  "cors": "^2.8.6",
63
63
  "express": "^5.2.1",
64
64
  "open": "^11.0.0",
65
- "@dimina-kit/compiler": "0.0.1-dev.20260702182435"
65
+ "@dimina-kit/compiler": "0.0.1-dev.20260703101348"
66
66
  },
67
67
  "publishConfig": {
68
68
  "access": "public"