@dimina-kit/devkit 0.1.2-dev.20260703051110 → 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,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;CACxC;AAuFD,wBAAsB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,cAAc,CAAC,CAqInF;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
@@ -114,7 +114,7 @@ async function cleanupFailedOpen(watcher, compileWorker, server) {
114
114
  }
115
115
  }
116
116
  export async function openProject(opts) {
117
- 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;
118
118
  const projectPath = path.resolve(rawProjectPath);
119
119
  const buildOptions = { sourcemap, fileTypes };
120
120
  const resolvedPort = port === 0 ? await getRandomPort() : port;
@@ -194,7 +194,9 @@ export async function openProject(opts) {
194
194
  // a build is in flight is never dropped: it coalesces into exactly one
195
195
  // trailing rebuild once the current run settles.
196
196
  const rebuildScheduler = createRebuildScheduler(() => runRebuild(compileWorker, buildRequest, sessionApps, () => reload, onRebuild, onBuildError));
197
- watcher = watch ? createProjectWatcher(projectPath, () => rebuildScheduler.schedule()) : null;
197
+ watcher = watch
198
+ ? createProjectWatcher(projectPath, () => rebuildScheduler.schedule(), onWatcherError)
199
+ : null;
198
200
  // Don't resolve until the watcher's initial scan is done: a save landing
199
201
  // in the gap between `openProject` resolving and chokidar going live
200
202
  // (fsevents stream not yet active on macOS) would otherwise be silently
@@ -232,7 +234,7 @@ export async function openProject(opts) {
232
234
  * watcher and whose `ready` resolves once the initial scan is complete (only
233
235
  * changes made after that point are guaranteed to surface).
234
236
  */
235
- export function createProjectWatcher(projectPath, onChange) {
237
+ export function createProjectWatcher(projectPath, onChange, onWatcherError) {
236
238
  // Ignore dotfiles/dot-directories that live *inside* the project (.git,
237
239
  // .DS_Store, editor scratch, etc.) without nuking the whole watch when the
238
240
  // project itself sits under a dotted ancestor path. A regex like
@@ -267,12 +269,24 @@ export function createProjectWatcher(projectPath, onChange) {
267
269
  // A watcher 'error' before the initial scan completes (EMFILE, permission
268
270
  // loss, …) must REJECT `ready`: resolving only on 'ready' would leave
269
271
  // `await watcher.ready` — and the whole openProject — hung forever. The
270
- // listener also doubles as the EventEmitter unhandled-'error' guard. A
271
- // 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;
272
278
  const ready = new Promise((resolve, reject) => {
273
- watcher.once('ready', () => resolve());
279
+ watcher.once('ready', () => {
280
+ readySettled = true;
281
+ resolve();
282
+ });
274
283
  watcher.on('error', (err) => {
275
- 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);
276
290
  });
277
291
  });
278
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.20260703051110",
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.20260703051110"
65
+ "@dimina-kit/compiler": "0.0.1-dev.20260703101348"
66
66
  },
67
67
  "publishConfig": {
68
68
  "access": "public"