@dimina-kit/devkit 0.1.2-dev.20260703101348 → 0.1.2-dev.20260706125036
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/README.md +23 -1
- package/dist/compile-worker-adopt.test.d.ts +2 -0
- package/dist/compile-worker-adopt.test.d.ts.map +1 -0
- package/dist/compile-worker-adopt.test.js +187 -0
- package/dist/compile-worker-entry-warmpool.test.d.ts +2 -0
- package/dist/compile-worker-entry-warmpool.test.d.ts.map +1 -0
- package/dist/compile-worker-entry-warmpool.test.js +110 -0
- package/dist/compile-worker-entry.d.ts +40 -8
- package/dist/compile-worker-entry.d.ts.map +1 -1
- package/dist/compile-worker-entry.js +50 -4
- package/dist/compile-worker-standby-hardening.test.d.ts +2 -0
- package/dist/compile-worker-standby-hardening.test.d.ts.map +1 -0
- package/dist/compile-worker-standby-hardening.test.js +212 -0
- package/dist/compile-worker-standby-integration.test.d.ts +2 -0
- package/dist/compile-worker-standby-integration.test.d.ts.map +1 -0
- package/dist/compile-worker-standby-integration.test.js +243 -0
- package/dist/compile-worker-standby.d.ts +60 -0
- package/dist/compile-worker-standby.d.ts.map +1 -0
- package/dist/compile-worker-standby.js +283 -0
- package/dist/compile-worker-standby.test.d.ts +2 -0
- package/dist/compile-worker-standby.test.d.ts.map +1 -0
- package/dist/compile-worker-standby.test.js +356 -0
- package/dist/compile-worker.d.ts +53 -0
- package/dist/compile-worker.d.ts.map +1 -1
- package/dist/compile-worker.js +30 -11
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +64 -9
- package/dist/open-project-compile-log.test.js +20 -8
- package/fe/dimina-fe-container/assets/index.js +8 -8
- package/fe/dimina-fe-container/assets/pageFrame.js +3 -3
- package/fe/dimina-fe-container/assets/service.js +2 -2
- package/fe/dimina-fe-container/dimina-version.json +2 -2
- package/package.json +3 -3
package/dist/compile-worker.d.ts
CHANGED
|
@@ -1,3 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parent-side orchestration of the forked compile worker.
|
|
3
|
+
*
|
|
4
|
+
* One long-lived worker per `openProject` session: the first compile and
|
|
5
|
+
* every watcher rebuild are `{ cmd: 'build' }` IPC messages to the SAME
|
|
6
|
+
* child, so the compiler's module/worker caches stay warm. The parent never
|
|
7
|
+
* calls `process.chdir` — the worker chdirs in its own process (the root
|
|
8
|
+
* motive of this architecture).
|
|
9
|
+
*
|
|
10
|
+
* Crash handling: an unexpected worker death ('exit', 'close' or a lone
|
|
11
|
+
* 'error' event — Node does NOT guarantee an 'exit' after 'error') rejects
|
|
12
|
+
* the in-flight build (so the rebuild scheduler settles instead of hanging)
|
|
13
|
+
* and the NEXT build lazily re-forks a fresh worker. The death handler is
|
|
14
|
+
* idempotent and generation-guarded: a dead child's late 'exit' can never
|
|
15
|
+
* settle a build that belongs to a fresh worker. `close()` kills, rejects
|
|
16
|
+
* the in-flight build itself, and resolves on the child's first death event
|
|
17
|
+
* ('exit'/'close', or a lone 'error') — it never re-forks
|
|
18
|
+
* (refill-on-graceful-close would wedge process teardown).
|
|
19
|
+
*/
|
|
20
|
+
import { type ChildProcess } from 'node:child_process';
|
|
1
21
|
import type { WorkerAppInfo, WorkerBuildOptions } from './compile-worker-entry.js';
|
|
2
22
|
export interface CompileLogEntry {
|
|
3
23
|
stream: 'stdout' | 'stderr';
|
|
@@ -6,6 +26,16 @@ export interface CompileLogEntry {
|
|
|
6
26
|
export interface CompileWorkerOptions {
|
|
7
27
|
/** Filtered dmcc log lines (already through `filterDmccLogLine`). */
|
|
8
28
|
onLog?: (entry: CompileLogEntry) => void;
|
|
29
|
+
/**
|
|
30
|
+
* An already-forked compile-worker-entry process to reuse for the first
|
|
31
|
+
* build (a warm-standby hand-off) instead of paying a fresh fork + compiler
|
|
32
|
+
* import. Must be an IPC fork with piped stdio — the same shape spawnWorker
|
|
33
|
+
* produces. Pure accelerator: an adoptee that is already dead (or dies
|
|
34
|
+
* before the first build) is simply skipped and the next build forks fresh.
|
|
35
|
+
* Once adopted, the process is owned by this worker exactly like a
|
|
36
|
+
* self-forked one — close() kills it, its death rejects in-flight builds.
|
|
37
|
+
*/
|
|
38
|
+
adopt?: ChildProcess;
|
|
9
39
|
}
|
|
10
40
|
export interface BuildRequest {
|
|
11
41
|
projectPath: string;
|
|
@@ -29,5 +59,28 @@ export interface CompileWorker {
|
|
|
29
59
|
*/
|
|
30
60
|
close: () => Promise<void>;
|
|
31
61
|
}
|
|
62
|
+
/**
|
|
63
|
+
* Resolve the fork target. In the published build this is the compiled
|
|
64
|
+
* `dist/compile-worker-entry.js` sibling; under vitest/dev the module runs
|
|
65
|
+
* from `src/`, where only the `.ts` source exists — the entry is a
|
|
66
|
+
* zero-dependency, erasable-syntax-only shell by design, so Node can run it
|
|
67
|
+
* with type stripping (default since 22.18; behind `--experimental-strip-types`
|
|
68
|
+
* on 22.6–22.17, which `workerExecArgv` passes for a `.ts` entry).
|
|
69
|
+
*
|
|
70
|
+
* Exported (with `workerExecArgv`) as the single authority on the fork shape:
|
|
71
|
+
* the warm-standby manager forks the SAME entry with the SAME execArgv, so a
|
|
72
|
+
* spare is indistinguishable from a worker this module forks itself.
|
|
73
|
+
*/
|
|
74
|
+
export declare function resolveWorkerEntry(): string;
|
|
75
|
+
/**
|
|
76
|
+
* execArgv for the fork. A compiled `.js` entry (the published build, engines
|
|
77
|
+
* `node >=20`) needs nothing. A `.ts` entry (vitest/dev only) needs Node type
|
|
78
|
+
* stripping: `--experimental-strip-types` exists since 22.6 — on older Node
|
|
79
|
+
* the child would die at startup with an opaque ERR_UNKNOWN_FILE_EXTENSION /
|
|
80
|
+
* bad-option crash, so fail HERE with an actionable message instead. The
|
|
81
|
+
* ExperimentalWarning is silenced because the child's stderr is the dmcc log
|
|
82
|
+
* transport. On ≥22.18 (stripping on by default) the flag is still accepted.
|
|
83
|
+
*/
|
|
84
|
+
export declare function workerExecArgv(entry: string): string[];
|
|
32
85
|
export declare function createCompileWorker(opts?: CompileWorkerOptions): CompileWorker;
|
|
33
86
|
//# sourceMappingURL=compile-worker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"compile-worker.d.ts","sourceRoot":"","sources":["../src/compile-worker.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"compile-worker.d.ts","sourceRoot":"","sources":["../src/compile-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAQ,KAAK,YAAY,EAAE,MAAM,oBAAoB,CAAA;AAM5D,OAAO,KAAK,EAAE,aAAa,EAAE,kBAAkB,EAAuB,MAAM,2BAA2B,CAAA;AAIvG,MAAM,WAAW,eAAe;IAC/B,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAA;IAC3B,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,oBAAoB;IACpC,qEAAqE;IACrE,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAA;IACxC;;;;;;;;OAQG;IACH,KAAK,CAAC,EAAE,YAAY,CAAA;CACpB;AAED,MAAM,WAAW,YAAY;IAC5B,WAAW,EAAE,MAAM,CAAA;IACnB,SAAS,EAAE,MAAM,CAAA;IACjB,OAAO,EAAE,kBAAkB,CAAA;CAC3B;AAED,MAAM,WAAW,aAAa;IAC7B;;;;;OAKG;IACH,KAAK,EAAE,CAAC,GAAG,EAAE,YAAY,KAAK,OAAO,CAAC,aAAa,GAAG,IAAI,CAAC,CAAA;IAC3D;;;;;;OAMG;IACH,KAAK,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;CAC1B;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,IAAI,MAAM,CAI3C;AAED;;;;;;;;GAQG;AACH,wBAAgB,cAAc,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAWtD;AAED,wBAAgB,mBAAmB,CAAC,IAAI,GAAE,oBAAyB,GAAG,aAAa,CA8LlF"}
|
package/dist/compile-worker.js
CHANGED
|
@@ -30,8 +30,12 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
|
30
30
|
* zero-dependency, erasable-syntax-only shell by design, so Node can run it
|
|
31
31
|
* with type stripping (default since 22.18; behind `--experimental-strip-types`
|
|
32
32
|
* on 22.6–22.17, which `workerExecArgv` passes for a `.ts` entry).
|
|
33
|
+
*
|
|
34
|
+
* Exported (with `workerExecArgv`) as the single authority on the fork shape:
|
|
35
|
+
* the warm-standby manager forks the SAME entry with the SAME execArgv, so a
|
|
36
|
+
* spare is indistinguishable from a worker this module forks itself.
|
|
33
37
|
*/
|
|
34
|
-
function resolveWorkerEntry() {
|
|
38
|
+
export function resolveWorkerEntry() {
|
|
35
39
|
const js = path.join(__dirname, 'compile-worker-entry.js');
|
|
36
40
|
if (fs.existsSync(js))
|
|
37
41
|
return js;
|
|
@@ -46,7 +50,7 @@ function resolveWorkerEntry() {
|
|
|
46
50
|
* ExperimentalWarning is silenced because the child's stderr is the dmcc log
|
|
47
51
|
* transport. On ≥22.18 (stripping on by default) the flag is still accepted.
|
|
48
52
|
*/
|
|
49
|
-
function workerExecArgv(entry) {
|
|
53
|
+
export function workerExecArgv(entry) {
|
|
50
54
|
if (!entry.endsWith('.ts'))
|
|
51
55
|
return [];
|
|
52
56
|
const [major = 0, minor = 0] = process.versions.node.split('.').map(Number);
|
|
@@ -112,15 +116,10 @@ export function createCompileWorker(opts = {}) {
|
|
|
112
116
|
stream.on('end', flush);
|
|
113
117
|
stream.on('close', flush);
|
|
114
118
|
}
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
silent: true,
|
|
120
|
-
// Explicit execArgv, NOT the parent's (vitest/electron loaders would
|
|
121
|
-
// leak into a plain Node child).
|
|
122
|
-
execArgv: workerExecArgv(entry),
|
|
123
|
-
});
|
|
119
|
+
// Attach the log pipes, result routing and death handling to a worker
|
|
120
|
+
// process — the SAME wiring whether the process was self-forked or adopted
|
|
121
|
+
// from a warm standby (an inline copy for one of the two would drift).
|
|
122
|
+
function wireWorker(worker) {
|
|
124
123
|
attachLineReader(worker.stdout, 'stdout');
|
|
125
124
|
attachLineReader(worker.stderr, 'stderr');
|
|
126
125
|
// Shared, idempotent death handler for 'exit' / 'close' / 'error': the
|
|
@@ -177,6 +176,26 @@ export function createCompileWorker(opts = {}) {
|
|
|
177
176
|
worker.on('error', (err) => settleDeath(`compile worker errored: ${err.message}`));
|
|
178
177
|
return worker;
|
|
179
178
|
}
|
|
179
|
+
function spawnWorker() {
|
|
180
|
+
const entry = resolveWorkerEntry();
|
|
181
|
+
return wireWorker(fork(entry, [], {
|
|
182
|
+
// Pipe stdout/stderr back to the parent — the dmcc log transport.
|
|
183
|
+
silent: true,
|
|
184
|
+
// Explicit execArgv, NOT the parent's (vitest/electron loaders would
|
|
185
|
+
// leak into a plain Node child).
|
|
186
|
+
execArgv: workerExecArgv(entry),
|
|
187
|
+
}));
|
|
188
|
+
}
|
|
189
|
+
// Adopt the warm-standby hand-off, if any, at creation time — wiring
|
|
190
|
+
// immediately (not at the first build) so a spare that dies in the gap is
|
|
191
|
+
// handled by the normal death path (child cleared → next build re-forks)
|
|
192
|
+
// instead of being discovered as a broken pipe mid-build. An adoptee that
|
|
193
|
+
// is ALREADY dead is skipped outright: its death events fired before any
|
|
194
|
+
// listener could attach, so wiring it would leave a permanently wedged
|
|
195
|
+
// child slot.
|
|
196
|
+
if (opts.adopt && opts.adopt.exitCode === null && opts.adopt.signalCode === null && opts.adopt.connected) {
|
|
197
|
+
child = wireWorker(opts.adopt);
|
|
198
|
+
}
|
|
180
199
|
function runBuild(req) {
|
|
181
200
|
if (closed) {
|
|
182
201
|
return Promise.reject(new Error('compile worker is closed'));
|
package/dist/index.d.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { CompileLogEntry } from './compile-worker.js';
|
|
2
|
+
import type { CompileWorkerStandby, CompileWorkerStandbyOptions } from './compile-worker-standby.js';
|
|
2
3
|
export { createRebuildScheduler } from './rebuild-scheduler.js';
|
|
3
4
|
export type { RebuildScheduler } from './rebuild-scheduler.js';
|
|
4
5
|
export { filterDmccLogLine } from './compile-log.js';
|
|
5
6
|
export { createCompileWorker } from './compile-worker.js';
|
|
6
7
|
export type { CompileLogEntry, CompileWorker } from './compile-worker.js';
|
|
8
|
+
export { createCompileWorkerStandby } from './compile-worker-standby.js';
|
|
9
|
+
export type { CompileWorkerStandby, CompileWorkerStandbyOptions, StandbyEvent, StandbyState, } from './compile-worker-standby.js';
|
|
10
|
+
/**
|
|
11
|
+
* Turn on the warm-standby accelerator and start warming the first spare
|
|
12
|
+
* immediately. Idempotent while the returned manager is live — repeat calls
|
|
13
|
+
* return the SAME manager (their opts are ignored). After `dispose()` (host
|
|
14
|
+
* shutdown, or a test tearing down) a new call builds a fresh manager. The
|
|
15
|
+
* caller owns disposal; a disposed manager makes every openProject fall back
|
|
16
|
+
* to cold forks and never spawns again.
|
|
17
|
+
*/
|
|
18
|
+
export declare function enableCompileWorkerStandby(opts?: CompileWorkerStandbyOptions): CompileWorkerStandby;
|
|
7
19
|
export interface AppInfo {
|
|
8
20
|
appId: string;
|
|
9
21
|
name: string;
|
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,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;
|
|
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,KAAK,EAAE,oBAAoB,EAAE,2BAA2B,EAAE,MAAM,6BAA6B,CAAA;AAEpG,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;AACzE,OAAO,EAAE,0BAA0B,EAAE,MAAM,6BAA6B,CAAA;AACxE,YAAY,EACX,oBAAoB,EACpB,2BAA2B,EAC3B,YAAY,EACZ,YAAY,GACZ,MAAM,6BAA6B,CAAA;AAYpC;;;;;;;GAOG;AACH,wBAAgB,0BAA0B,CACzC,IAAI,CAAC,EAAE,2BAA2B,GAChC,oBAAoB,CAKtB;AAqDD,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,CA0JnF;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
|
@@ -8,9 +8,46 @@ import { createRequire } from 'node:module';
|
|
|
8
8
|
import chokidar from 'chokidar';
|
|
9
9
|
import { createRebuildScheduler } from './rebuild-scheduler.js';
|
|
10
10
|
import { createCompileWorker } from './compile-worker.js';
|
|
11
|
+
import { createCompileWorkerStandby } from './compile-worker-standby.js';
|
|
11
12
|
export { createRebuildScheduler } from './rebuild-scheduler.js';
|
|
12
13
|
export { filterDmccLogLine } from './compile-log.js';
|
|
13
14
|
export { createCompileWorker } from './compile-worker.js';
|
|
15
|
+
export { createCompileWorkerStandby } from './compile-worker-standby.js';
|
|
16
|
+
// ── warm standby (opt-in) ──────────────────────────────────────────────────
|
|
17
|
+
//
|
|
18
|
+
// One module-level spare compile worker shared by every `openProject` in this
|
|
19
|
+
// process. While no project is open the spare sits forked + prewarmed
|
|
20
|
+
// (project-agnostic — it never chdirs, see compile-worker-entry); openProject
|
|
21
|
+
// adopts it for the first compile and each session close refills it. Purely
|
|
22
|
+
// additive: hosts that never call `enableCompileWorkerStandby` get exactly the
|
|
23
|
+
// cold-fork behavior they had before.
|
|
24
|
+
let standbyManager = null;
|
|
25
|
+
/**
|
|
26
|
+
* Turn on the warm-standby accelerator and start warming the first spare
|
|
27
|
+
* immediately. Idempotent while the returned manager is live — repeat calls
|
|
28
|
+
* return the SAME manager (their opts are ignored). After `dispose()` (host
|
|
29
|
+
* shutdown, or a test tearing down) a new call builds a fresh manager. The
|
|
30
|
+
* caller owns disposal; a disposed manager makes every openProject fall back
|
|
31
|
+
* to cold forks and never spawns again.
|
|
32
|
+
*/
|
|
33
|
+
export function enableCompileWorkerStandby(opts) {
|
|
34
|
+
if (standbyManager && standbyManager.state !== 'disposed')
|
|
35
|
+
return standbyManager;
|
|
36
|
+
standbyManager = createCompileWorkerStandby(opts);
|
|
37
|
+
standbyManager.ensure();
|
|
38
|
+
return standbyManager;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Refill the spare after a compile worker has been consumed and fully closed.
|
|
42
|
+
* Fire-and-forget BY DESIGN: the fork happens asynchronously inside the
|
|
43
|
+
* manager, so a session close never waits on (and can never be wedged by)
|
|
44
|
+
* standby work — the refill-on-graceful-close deadlock class stays
|
|
45
|
+
* structurally impossible. No-ops when the standby is off, already warming,
|
|
46
|
+
* or disposed.
|
|
47
|
+
*/
|
|
48
|
+
function refillStandby() {
|
|
49
|
+
standbyManager?.ensure();
|
|
50
|
+
}
|
|
14
51
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
15
52
|
const require = createRequire(import.meta.url);
|
|
16
53
|
// esbuild's native binary is shipped inside node_modules, but when packaged
|
|
@@ -125,8 +162,12 @@ export async function openProject(opts) {
|
|
|
125
162
|
// Compilation runs in a long-lived forked worker — the worker chdirs in
|
|
126
163
|
// its OWN process, so this (host) process never mutates its cwd, and dmcc
|
|
127
164
|
// terminal output arrives on the worker's piped streams (delivered to
|
|
128
|
-
// `onLog` line-by-line after `filterDmccLogLine`).
|
|
129
|
-
|
|
165
|
+
// `onLog` line-by-line after `filterDmccLogLine`). When the warm standby is
|
|
166
|
+
// enabled and holds a healthy spare, adopt it: the first compile then runs
|
|
167
|
+
// on an already-forked, toolchain-warm process. adopt() degrades to null on
|
|
168
|
+
// every failure path, which lands in the normal cold-fork behavior.
|
|
169
|
+
const adopted = standbyManager ? await standbyManager.adopt() : null;
|
|
170
|
+
const compileWorker = createCompileWorker({ onLog, ...(adopted ? { adopt: adopted } : {}) });
|
|
130
171
|
const buildRequest = {
|
|
131
172
|
projectPath,
|
|
132
173
|
outputDir: resolvedOutputDir,
|
|
@@ -137,13 +178,22 @@ export async function openProject(opts) {
|
|
|
137
178
|
initialAppInfo = (await compileWorker.build(buildRequest));
|
|
138
179
|
}
|
|
139
180
|
catch (err) {
|
|
181
|
+
// The first compile failing IS the open failing: a compile error rejects
|
|
182
|
+
// out of the worker (the pool relays it as a real rejection), and a
|
|
183
|
+
// session started anyway could only serve 404s (no artifacts in
|
|
184
|
+
// outputDir) while the real cause stays invisible. Tear down and rethrow
|
|
185
|
+
// so the host sees the compile error as openProject's failure.
|
|
140
186
|
await compileWorker.close();
|
|
187
|
+
// A failed open consumed the spare too — refill (fire-and-forget) so the
|
|
188
|
+
// NEXT open attempt still starts warm.
|
|
189
|
+
refillStandby();
|
|
141
190
|
throw err;
|
|
142
191
|
}
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
192
|
+
// A compile FAILURE rejects above — a resolved null never means "compile
|
|
193
|
+
// failed". It means the compiler had no app info to report: e.g. racing a
|
|
194
|
+
// project that was just closed elsewhere in the same Electron process,
|
|
195
|
+
// where the manifest on disk is perfectly readable. The fallback to
|
|
196
|
+
// `{ appId: 'unknown' }` is load-bearing for
|
|
147
197
|
// any consumer that calls `wx.setStorageSync` — the dimina runtime
|
|
148
198
|
// stores values under `${appId}_${key}` and the devtools storage panel
|
|
149
199
|
// also derives its IPC prefix from `appInfo.appId`. A stale `unknown`
|
|
@@ -164,7 +214,7 @@ export async function openProject(opts) {
|
|
|
164
214
|
// real cause here, at the point the invalid id is produced, instead of
|
|
165
215
|
// letting it fail opaquely at injection time.
|
|
166
216
|
const reason = `[devkit] could not resolve an appId for ${projectPath}: the compiler produced no app info and project.config.json has no "appid". `
|
|
167
|
-
+ 'Falling back to "unknown" — the mini-program likely
|
|
217
|
+
+ 'Falling back to "unknown" — the mini-program is likely missing its manifest.';
|
|
168
218
|
console.warn(reason);
|
|
169
219
|
onBuildError?.(new Error(reason));
|
|
170
220
|
}
|
|
@@ -207,6 +257,7 @@ export async function openProject(opts) {
|
|
|
207
257
|
}
|
|
208
258
|
catch (err) {
|
|
209
259
|
await cleanupFailedOpen(watcher, compileWorker, server);
|
|
260
|
+
refillStandby();
|
|
210
261
|
throw err;
|
|
211
262
|
}
|
|
212
263
|
// `server` is always assigned when the try block completes without throwing
|
|
@@ -220,9 +271,13 @@ export async function openProject(opts) {
|
|
|
220
271
|
close: async () => {
|
|
221
272
|
await watcher?.close();
|
|
222
273
|
// Kill the long-lived compile worker and WAIT for the child to be
|
|
223
|
-
// actually dead —
|
|
224
|
-
// refill here would wedge process teardown).
|
|
274
|
+
// actually dead — THIS worker is never re-forked on a graceful close
|
|
275
|
+
// (an awaited refill here would wedge process teardown).
|
|
225
276
|
await compileWorker.close();
|
|
277
|
+
// The standby refill is different: fire-and-forget into the manager
|
|
278
|
+
// (nothing here waits on it), sequenced after the old worker's death
|
|
279
|
+
// so spare + dying worker never hold memory simultaneously.
|
|
280
|
+
refillStandby();
|
|
226
281
|
liveServer.closeAllConnections?.();
|
|
227
282
|
await new Promise(resolve => liveServer.close(() => resolve()));
|
|
228
283
|
},
|
|
@@ -54,17 +54,29 @@ function expectNoNoise(texts) {
|
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
describe('openProject onLog — first compile (integration, real dmcc)', () => {
|
|
57
|
-
it('
|
|
57
|
+
it('rejects openProject itself when the first compile fails, after streaming every filtered log line to onLog', async () => {
|
|
58
58
|
const root = makeFixture({ brokenIndexJs: true });
|
|
59
59
|
const entries = [];
|
|
60
60
|
const logOpts = { onLog: (entry) => entries.push(entry) };
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
61
|
+
let caught;
|
|
62
|
+
try {
|
|
63
|
+
const session = await devkit.openProject({
|
|
64
|
+
projectPath: root,
|
|
65
|
+
watch: false,
|
|
66
|
+
outputDir: path.join(root, '.out'),
|
|
67
|
+
...logOpts,
|
|
68
|
+
});
|
|
69
|
+
// Only reached if openProject wrongly resolved — track it for teardown
|
|
70
|
+
// so a false-negative pass here still doesn't leak a dev server/watcher.
|
|
71
|
+
openSessions.push(session);
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
caught = err;
|
|
75
|
+
}
|
|
76
|
+
expect(caught, 'openProject must REJECT when the first compile fails — a broken project must never silently open a session').toBeInstanceOf(Error);
|
|
77
|
+
const message = caught.message;
|
|
78
|
+
expect(message, 'the rejection must name the failing stage, not a generic wrapper message').toMatch(/stage "logic" failed/);
|
|
79
|
+
expect(message, 'the rejection must carry the underlying compile reason (esbuild transform failure), not just the stage name').toMatch(/Transform failed/);
|
|
68
80
|
expect(entries.length, 'openProject must wire opts.onLog around the first build() — no lines were captured').toBeGreaterThan(0);
|
|
69
81
|
const texts = entries.map(entry => entry.text);
|
|
70
82
|
expect(texts.some(text => /esbuild 转换失败/.test(text)), 'the `[logic] esbuild 转换失败 <file>` line is the highest-value error line and must be kept').toBe(true);
|