@pygmalionjs/pygmalion 0.6.2 → 0.6.4
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-lib/{FrozenRoutePreview-X4XRmf9G.js → FrozenRoutePreview-V9-gxIyv.js} +152 -172
- package/dist-lib/pygmalion.js +5196 -5042
- package/dist-lib/testing.js +1 -1
- package/dist-lib/types/canvas/ObjectControls.d.ts +5 -8
- package/dist-lib/types/editor/previewCaptureProgress.d.ts +47 -0
- package/dist-lib/types/editor/projectRuntime.d.ts +0 -5
- package/dist-lib/types/editor/store.d.ts +0 -14
- package/dist-lib/types/lib.d.ts +2 -0
- package/node/dev-mirror.mjs +8 -79
- package/node/preview-artifact-plugin.mjs +203 -49
- package/node/preview-capture-worker.mjs +80 -7
- package/node/vite.mjs +2 -0
- package/package.json +1 -1
- package/vite.d.ts +40 -18
|
@@ -10,6 +10,13 @@ const DEFAULT_PING_TIMEOUT_MS = 10_000;
|
|
|
10
10
|
const MAX_RESTART_BACKOFF_MS = 8_000;
|
|
11
11
|
const STOP_KILL_GRACE_MS = 5_000;
|
|
12
12
|
const STDERR_TAIL_LIMIT = 8_000;
|
|
13
|
+
const PROGRESS_PHASES = new Set([
|
|
14
|
+
'queued',
|
|
15
|
+
'preparing',
|
|
16
|
+
'capturing',
|
|
17
|
+
'retrying',
|
|
18
|
+
'finalizing',
|
|
19
|
+
]);
|
|
13
20
|
|
|
14
21
|
function assertPositiveNumber(value, name, fallback) {
|
|
15
22
|
if (value == null) return fallback;
|
|
@@ -127,6 +134,34 @@ function workerFailure(worker, fallbackMessage) {
|
|
|
127
134
|
return new Error(tail ? `${fallbackMessage}\n${tail}` : fallbackMessage);
|
|
128
135
|
}
|
|
129
136
|
|
|
137
|
+
function normalizeProgressMessage(message) {
|
|
138
|
+
if (message?.type !== 'progress' || !PROGRESS_PHASES.has(message.phase)) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
const completed = Number(message.completed);
|
|
142
|
+
const total = Number(message.total);
|
|
143
|
+
if (
|
|
144
|
+
!Number.isInteger(completed) ||
|
|
145
|
+
!Number.isInteger(total) ||
|
|
146
|
+
completed < 0 ||
|
|
147
|
+
total < 0 ||
|
|
148
|
+
completed > total
|
|
149
|
+
) {
|
|
150
|
+
return null;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
phase: message.phase,
|
|
154
|
+
completed,
|
|
155
|
+
total,
|
|
156
|
+
...(typeof message.frameId === 'string' && message.frameId.trim()
|
|
157
|
+
? { frameId: message.frameId.trim() }
|
|
158
|
+
: {}),
|
|
159
|
+
...(typeof message.captureStatus === 'string' && message.captureStatus.trim()
|
|
160
|
+
? { captureStatus: message.captureStatus.trim() }
|
|
161
|
+
: {}),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
130
165
|
/**
|
|
131
166
|
* Long-lived capture worker channel: one child process that keeps its expensive
|
|
132
167
|
* boot (runtime preflight, case bundling, browser launch) warm across artifact
|
|
@@ -134,6 +169,7 @@ function workerFailure(worker, fallbackMessage) {
|
|
|
134
169
|
*
|
|
135
170
|
* Protocol: the channel writes `{ id, type: 'capture', payload }` — payload is
|
|
136
171
|
* `{ namespace, sourceRevision, frames?, captureBaseUrl? }` — and the worker
|
|
172
|
+
* may emit `{ id, type: 'progress', phase, completed, total, frameId? }`, then
|
|
137
173
|
* answers `{ id, ok: true, artifactFile }` with the bundle in a temp file the
|
|
138
174
|
* channel reads, validates, and deletes, or `{ id, ok: false, error }`.
|
|
139
175
|
* `{ id, type: 'ping' }` must be answered immediately even while booting; the
|
|
@@ -266,6 +302,17 @@ export function createPreviewCaptureWorkerChannel({
|
|
|
266
302
|
settlePending(current, ({ resolve }) => resolve(message));
|
|
267
303
|
return;
|
|
268
304
|
}
|
|
305
|
+
if (message.type === 'progress') {
|
|
306
|
+
const progress = normalizeProgressMessage(message);
|
|
307
|
+
if (progress && pending.onProgress) {
|
|
308
|
+
try {
|
|
309
|
+
pending.onProgress(progress);
|
|
310
|
+
} catch {
|
|
311
|
+
// Progress observation must never fail the capture it observes.
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
269
316
|
if (message.ok === true) {
|
|
270
317
|
settlePending(current, ({ resolve }) => resolve(message));
|
|
271
318
|
} else {
|
|
@@ -324,7 +371,11 @@ export function createPreviewCaptureWorkerChannel({
|
|
|
324
371
|
current.proc.kill('SIGKILL');
|
|
325
372
|
}
|
|
326
373
|
|
|
327
|
-
function dispatch(
|
|
374
|
+
function dispatch(
|
|
375
|
+
current,
|
|
376
|
+
message,
|
|
377
|
+
{ kind, timeoutMs, onTimeout, onProgress },
|
|
378
|
+
) {
|
|
328
379
|
return new Promise((resolve, reject) => {
|
|
329
380
|
const timer = setTimeout(() => {
|
|
330
381
|
current.pending = null;
|
|
@@ -339,7 +390,14 @@ export function createPreviewCaptureWorkerChannel({
|
|
|
339
390
|
);
|
|
340
391
|
}, timeoutMs);
|
|
341
392
|
timer.unref?.();
|
|
342
|
-
current.pending = {
|
|
393
|
+
current.pending = {
|
|
394
|
+
id: message.id,
|
|
395
|
+
kind,
|
|
396
|
+
resolve,
|
|
397
|
+
reject,
|
|
398
|
+
timer,
|
|
399
|
+
onProgress,
|
|
400
|
+
};
|
|
343
401
|
try {
|
|
344
402
|
current.proc.stdin.write(`${JSON.stringify(message)}\n`);
|
|
345
403
|
} catch (error) {
|
|
@@ -400,13 +458,14 @@ export function createPreviewCaptureWorkerChannel({
|
|
|
400
458
|
return worker;
|
|
401
459
|
}
|
|
402
460
|
|
|
403
|
-
function runWorkerJob(current, payload) {
|
|
461
|
+
function runWorkerJob(current, payload, onProgress) {
|
|
404
462
|
return dispatch(
|
|
405
463
|
current,
|
|
406
464
|
{ id: nextRequestId(), type: 'capture', payload },
|
|
407
465
|
{
|
|
408
466
|
kind: 'capture',
|
|
409
467
|
timeoutMs: resolvedJobTimeoutMs,
|
|
468
|
+
onProgress,
|
|
410
469
|
onTimeout: () => {
|
|
411
470
|
recordCrash();
|
|
412
471
|
killWorker(current);
|
|
@@ -419,7 +478,7 @@ export function createPreviewCaptureWorkerChannel({
|
|
|
419
478
|
* Degraded path: one spawn per request with `--once`, the same protocol over
|
|
420
479
|
* the child's own stdio. Slow again, but on-demand capture keeps answering.
|
|
421
480
|
*/
|
|
422
|
-
function runOnceJob(payload) {
|
|
481
|
+
function runOnceJob(payload, onProgress) {
|
|
423
482
|
return new Promise((resolve, reject) => {
|
|
424
483
|
const proc = spawn(
|
|
425
484
|
process.execPath,
|
|
@@ -450,6 +509,17 @@ export function createPreviewCaptureWorkerChannel({
|
|
|
450
509
|
const id = nextRequestId();
|
|
451
510
|
attachLineParser(proc.stdout, (message) => {
|
|
452
511
|
if (message.id !== id) return;
|
|
512
|
+
if (message.type === 'progress') {
|
|
513
|
+
const progress = normalizeProgressMessage(message);
|
|
514
|
+
if (progress && onProgress) {
|
|
515
|
+
try {
|
|
516
|
+
onProgress(progress);
|
|
517
|
+
} catch {
|
|
518
|
+
// Progress observation must never fail the capture it observes.
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
453
523
|
if (message.ok === true) {
|
|
454
524
|
settle(() => resolve(message));
|
|
455
525
|
} else {
|
|
@@ -493,10 +563,13 @@ export function createPreviewCaptureWorkerChannel({
|
|
|
493
563
|
});
|
|
494
564
|
}
|
|
495
565
|
|
|
496
|
-
async function generateArtifact(request) {
|
|
566
|
+
async function generateArtifact(request, { onProgress } = {}) {
|
|
497
567
|
if (disposed) {
|
|
498
568
|
throw new Error('Preview capture worker channel is disposed.');
|
|
499
569
|
}
|
|
570
|
+
if (onProgress != null && typeof onProgress !== 'function') {
|
|
571
|
+
throw new TypeError('Preview capture worker onProgress must be a function.');
|
|
572
|
+
}
|
|
500
573
|
// The artifact plugin serializes per identity; overlap here means a caller
|
|
501
574
|
// bypassed that queue, and interleaving two captures on one warm browser
|
|
502
575
|
// would let them poison each other.
|
|
@@ -509,8 +582,8 @@ export function createPreviewCaptureWorkerChannel({
|
|
|
509
582
|
try {
|
|
510
583
|
const current = degraded ? null : await ensureWorker();
|
|
511
584
|
const response = current
|
|
512
|
-
? await runWorkerJob(current, payload)
|
|
513
|
-
: await runOnceJob(payload);
|
|
585
|
+
? await runWorkerJob(current, payload, onProgress)
|
|
586
|
+
: await runOnceJob(payload, onProgress);
|
|
514
587
|
return await consumeArtifactFile(response);
|
|
515
588
|
} finally {
|
|
516
589
|
inFlight = false;
|
package/node/vite.mjs
CHANGED
|
@@ -67,6 +67,7 @@ import { resolvePreviewRouteDependencyDigest } from './route-dependency-digest.m
|
|
|
67
67
|
import {
|
|
68
68
|
DEFAULT_PYGMALION_PREVIEW_ARTIFACT_FILE,
|
|
69
69
|
PYGMALION_PREVIEW_ARTIFACT_ENDPOINT,
|
|
70
|
+
PYGMALION_PREVIEW_CAPTURE_PROGRESS_SUFFIX,
|
|
70
71
|
pygmalionPreviewArtifactPlugin,
|
|
71
72
|
} from './preview-artifact-plugin.mjs';
|
|
72
73
|
import { createPreviewCaptureWorkerChannel } from './preview-capture-worker.mjs';
|
|
@@ -433,6 +434,7 @@ export {
|
|
|
433
434
|
validateRoutePreviewArtifactV3,
|
|
434
435
|
DEFAULT_PYGMALION_PREVIEW_ARTIFACT_FILE,
|
|
435
436
|
PYGMALION_PREVIEW_ARTIFACT_ENDPOINT,
|
|
437
|
+
PYGMALION_PREVIEW_CAPTURE_PROGRESS_SUFFIX,
|
|
436
438
|
pygmalionPreviewArtifactPlugin,
|
|
437
439
|
createPreviewCaptureWorkerChannel,
|
|
438
440
|
DEFAULT_QA_CAPTURE_MAX_FRAMES,
|
package/package.json
CHANGED
package/vite.d.ts
CHANGED
|
@@ -71,6 +71,18 @@ export interface PygmalionPreviewCaptureWorkerConfig {
|
|
|
71
71
|
jobTimeoutMs?: number;
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
export interface PygmalionPreviewCaptureProgressEvent {
|
|
75
|
+
phase: 'queued' | 'preparing' | 'capturing' | 'retrying' | 'finalizing';
|
|
76
|
+
completed: number;
|
|
77
|
+
total: number;
|
|
78
|
+
frameId?: string;
|
|
79
|
+
captureStatus?: string;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface PygmalionPreviewCaptureGeneratorOptions {
|
|
83
|
+
onProgress?: (progress: PygmalionPreviewCaptureProgressEvent) => void;
|
|
84
|
+
}
|
|
85
|
+
|
|
74
86
|
export interface PygmalionPreviewConfig {
|
|
75
87
|
configFile?: string;
|
|
76
88
|
viteConfig?: string;
|
|
@@ -88,12 +100,15 @@ export interface PygmalionPreviewConfig {
|
|
|
88
100
|
sessionPort?: number;
|
|
89
101
|
artifactFile?: string;
|
|
90
102
|
artifactEndpoint?: string;
|
|
91
|
-
generateArtifact?: (
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
103
|
+
generateArtifact?: (
|
|
104
|
+
request: {
|
|
105
|
+
namespace: string;
|
|
106
|
+
sourceRevision: string;
|
|
107
|
+
frames?: readonly { id: string; fingerprint?: string }[];
|
|
108
|
+
captureBaseUrl?: string;
|
|
109
|
+
},
|
|
110
|
+
options?: PygmalionPreviewCaptureGeneratorOptions,
|
|
111
|
+
) => unknown | Promise<unknown>;
|
|
97
112
|
/**
|
|
98
113
|
* Warm capture worker used to satisfy artifact generation requests when
|
|
99
114
|
* `generateArtifact` is absent. When both are set, `generateArtifact` wins.
|
|
@@ -170,18 +185,22 @@ export declare function createPygmalionVitePlugins(
|
|
|
170
185
|
): PluginOption[];
|
|
171
186
|
|
|
172
187
|
export declare const PYGMALION_PREVIEW_ARTIFACT_ENDPOINT: string;
|
|
188
|
+
export declare const PYGMALION_PREVIEW_CAPTURE_PROGRESS_SUFFIX: string;
|
|
173
189
|
export declare const DEFAULT_PYGMALION_PREVIEW_ARTIFACT_FILE: string;
|
|
174
190
|
export declare function pygmalionPreviewArtifactPlugin(options?: {
|
|
175
191
|
root?: string;
|
|
176
192
|
artifactFile?: string;
|
|
177
193
|
endpoint?: string;
|
|
178
194
|
disabled?: () => boolean;
|
|
179
|
-
generateArtifact?: (
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
195
|
+
generateArtifact?: (
|
|
196
|
+
request: {
|
|
197
|
+
namespace: string;
|
|
198
|
+
sourceRevision: string;
|
|
199
|
+
frames?: readonly { id: string; fingerprint?: string }[];
|
|
200
|
+
captureBaseUrl?: string;
|
|
201
|
+
},
|
|
202
|
+
options?: PygmalionPreviewCaptureGeneratorOptions,
|
|
203
|
+
) => unknown | Promise<unknown>;
|
|
185
204
|
acquireLease?: (
|
|
186
205
|
reason: string,
|
|
187
206
|
) =>
|
|
@@ -190,12 +209,15 @@ export declare function pygmalionPreviewArtifactPlugin(options?: {
|
|
|
190
209
|
| Promise<{ release(): void | Promise<void> } | null>;
|
|
191
210
|
}): PluginOption;
|
|
192
211
|
export interface PygmalionPreviewCaptureWorkerChannel {
|
|
193
|
-
generateArtifact(
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
212
|
+
generateArtifact(
|
|
213
|
+
request: {
|
|
214
|
+
namespace: string;
|
|
215
|
+
sourceRevision: string;
|
|
216
|
+
frames?: readonly { id: string; fingerprint?: string }[];
|
|
217
|
+
captureBaseUrl?: string;
|
|
218
|
+
},
|
|
219
|
+
options?: PygmalionPreviewCaptureGeneratorOptions,
|
|
220
|
+
): Promise<unknown>;
|
|
199
221
|
dispose(): Promise<void>;
|
|
200
222
|
}
|
|
201
223
|
export declare function createPreviewCaptureWorkerChannel(
|