@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 +9 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +99 -57
- package/dist/open-project-watcher-error-callback.test.d.ts +2 -0
- package/dist/open-project-watcher-error-callback.test.d.ts.map +1 -0
- package/dist/open-project-watcher-error-callback.test.js +191 -0
- package/package.json +2 -2
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
|
};
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,KAAK,
|
|
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
|
-
|
|
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(
|
|
152
|
-
watcher = watch
|
|
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
|
-
|
|
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.
|
|
243
|
-
//
|
|
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', () =>
|
|
279
|
+
watcher.once('ready', () => {
|
|
280
|
+
readySettled = true;
|
|
281
|
+
resolve();
|
|
282
|
+
});
|
|
246
283
|
watcher.on('error', (err) => {
|
|
247
|
-
|
|
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 @@
|
|
|
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.
|
|
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.
|
|
65
|
+
"@dimina-kit/compiler": "0.0.1-dev.20260703101348"
|
|
66
66
|
},
|
|
67
67
|
"publishConfig": {
|
|
68
68
|
"access": "public"
|