@pygmalionjs/pygmalion 0.2.20 → 0.2.21
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/{App-BncfvzKi.js → App-B_6qmxhS.js} +4 -3
- package/dist-lib/pygmalion.js +302 -288
- package/dist-lib/testing.js +1 -1
- package/node/preview-artifact-plugin.mjs +0 -0
- package/node/route-preview-artifact-v3.mjs +161 -0
- package/package.json +1 -1
- package/storyboard.d.ts +18 -0
- package/types.d.ts +11 -0
- package/vite.d.ts +3 -0
package/dist-lib/testing.js
CHANGED
|
Binary file
|
|
@@ -27,6 +27,7 @@ export const ROUTE_PREVIEW_ARTIFACT_V3_LIMITS = Object.freeze({
|
|
|
27
27
|
diagnosticCountPerFrame: 256,
|
|
28
28
|
identifierLength: 512,
|
|
29
29
|
tokenLength: 128,
|
|
30
|
+
fingerprintLength: 256,
|
|
30
31
|
selectorLength: 4_096,
|
|
31
32
|
labelLength: 1_024,
|
|
32
33
|
messageLength: 4_096,
|
|
@@ -65,6 +66,23 @@ function validOptionalIdentifier(value) {
|
|
|
65
66
|
return value == null || validIdentifier(value);
|
|
66
67
|
}
|
|
67
68
|
|
|
69
|
+
/**
|
|
70
|
+
* A frame fingerprint is opaque to this layer: the consumer decides what a frame
|
|
71
|
+
* depends on. Only shape is enforced, so a hash, a revision, or a composite key
|
|
72
|
+
* all pass, and none of them can smuggle in a record key.
|
|
73
|
+
*/
|
|
74
|
+
function validFingerprint(value) {
|
|
75
|
+
return (
|
|
76
|
+
typeof value === 'string' &&
|
|
77
|
+
value.length > 0 &&
|
|
78
|
+
value.length <= ROUTE_PREVIEW_ARTIFACT_V3_LIMITS.fingerprintLength &&
|
|
79
|
+
value === value.trim() &&
|
|
80
|
+
// Hyphens and colons are ordinary in a hash or a composite key, so only
|
|
81
|
+
// whitespace and control characters are refused.
|
|
82
|
+
!/[\s\u0000-\u001f\u007f]/u.test(value)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
68
86
|
function validViewport(value) {
|
|
69
87
|
return (
|
|
70
88
|
isPlainRecord(value) &&
|
|
@@ -380,6 +398,12 @@ export function createRoutePreviewArtifactV3(input) {
|
|
|
380
398
|
{
|
|
381
399
|
status: rawCapture.status,
|
|
382
400
|
viewport: copyViewport(rawCapture.viewport),
|
|
401
|
+
// What this frame was captured from. A consumer compares its own
|
|
402
|
+
// fingerprint against this one, so a frame survives a revision bump
|
|
403
|
+
// whose files it does not depend on.
|
|
404
|
+
...(rawCapture.fingerprint == null
|
|
405
|
+
? {}
|
|
406
|
+
: { fingerprint: String(rawCapture.fingerprint) }),
|
|
383
407
|
...(compact.frames[frameId] == null
|
|
384
408
|
? {}
|
|
385
409
|
: { snapshot: compact.frames[frameId] }),
|
|
@@ -459,6 +483,13 @@ export function validateRoutePreviewArtifactV3(bundle) {
|
|
|
459
483
|
errors.push(`Frame "${frameId}" status or viewport is invalid.`);
|
|
460
484
|
continue;
|
|
461
485
|
}
|
|
486
|
+
if (
|
|
487
|
+
rawFrame.fingerprint !== undefined &&
|
|
488
|
+
!validFingerprint(rawFrame.fingerprint)
|
|
489
|
+
) {
|
|
490
|
+
errors.push(`Frame "${frameId}" fingerprint is invalid.`);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
462
493
|
const diagnostics = rawFrame.diagnostics;
|
|
463
494
|
if (
|
|
464
495
|
!Array.isArray(diagnostics) ||
|
|
@@ -543,6 +574,136 @@ export function validateRoutePreviewArtifactBundle(bundle) {
|
|
|
543
574
|
: validateRoutePreviewArtifactV2(bundle);
|
|
544
575
|
}
|
|
545
576
|
|
|
577
|
+
/**
|
|
578
|
+
* Assets a set of frames still needs. A frame lists its head and stylesheet
|
|
579
|
+
* assets by hash, so a subset or a merge can drop what nothing references
|
|
580
|
+
* instead of carrying every asset the bundle ever held.
|
|
581
|
+
*/
|
|
582
|
+
function referencedAssets(frames) {
|
|
583
|
+
const head = new Set();
|
|
584
|
+
const stylesheets = new Set();
|
|
585
|
+
const screenshots = new Set();
|
|
586
|
+
for (const frame of Object.values(frames)) {
|
|
587
|
+
if (!isPlainRecord(frame)) continue;
|
|
588
|
+
for (const reference of frame.snapshot?.head ?? []) {
|
|
589
|
+
if (!isPlainRecord(reference) || typeof reference.hash !== 'string') continue;
|
|
590
|
+
(reference.kind === 'stylesheet' ? stylesheets : head).add(reference.hash);
|
|
591
|
+
}
|
|
592
|
+
if (isPlainRecord(frame.screenshot) && typeof frame.screenshot.hash === 'string') {
|
|
593
|
+
screenshots.add(frame.screenshot.hash);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
return { head, stylesheets, screenshots };
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function keepAssets(record, keys) {
|
|
600
|
+
const kept = {};
|
|
601
|
+
for (const key of Object.keys(record ?? {}).sort()) {
|
|
602
|
+
if (keys.has(key)) kept[key] = record[key];
|
|
603
|
+
}
|
|
604
|
+
return kept;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
function withFrames(bundle, frames) {
|
|
608
|
+
const used = referencedAssets(frames);
|
|
609
|
+
return {
|
|
610
|
+
version: 3,
|
|
611
|
+
namespace: bundle.namespace,
|
|
612
|
+
...(bundle.sourceRevision == null ? {} : { sourceRevision: bundle.sourceRevision }),
|
|
613
|
+
assets: {
|
|
614
|
+
head: keepAssets(bundle.assets?.head, used.head),
|
|
615
|
+
stylesheets: keepAssets(bundle.assets?.stylesheets, used.stylesheets),
|
|
616
|
+
screenshots: keepAssets(bundle.assets?.screenshots, used.screenshots),
|
|
617
|
+
},
|
|
618
|
+
frames,
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Picks the frames a consumer asked for and reports what it must still capture.
|
|
624
|
+
*
|
|
625
|
+
* A frame is usable when it exists and its stored fingerprint matches the
|
|
626
|
+
* requested one. Requests without a fingerprint accept whatever is stored, which
|
|
627
|
+
* keeps the whole-bundle callers of earlier versions working.
|
|
628
|
+
*/
|
|
629
|
+
export function selectRoutePreviewArtifactFrames(bundle, wanted) {
|
|
630
|
+
if (!isPlainRecord(bundle) || !isPlainRecord(bundle.frames)) {
|
|
631
|
+
throw new TypeError('Route preview artifact is invalid.');
|
|
632
|
+
}
|
|
633
|
+
if (!Array.isArray(wanted)) {
|
|
634
|
+
throw new TypeError('Wanted frames must be an array.');
|
|
635
|
+
}
|
|
636
|
+
const frames = {};
|
|
637
|
+
const missing = [];
|
|
638
|
+
const stale = [];
|
|
639
|
+
for (const request of wanted) {
|
|
640
|
+
const id = isPlainRecord(request) ? request.id : request;
|
|
641
|
+
if (typeof id !== 'string' || !id) {
|
|
642
|
+
throw new TypeError('Wanted frame id must be a non-empty string.');
|
|
643
|
+
}
|
|
644
|
+
const frame = Object.hasOwn(bundle.frames, id) ? bundle.frames[id] : null;
|
|
645
|
+
if (!isPlainRecord(frame)) {
|
|
646
|
+
missing.push(id);
|
|
647
|
+
continue;
|
|
648
|
+
}
|
|
649
|
+
const fingerprint = isPlainRecord(request) ? request.fingerprint : undefined;
|
|
650
|
+
if (fingerprint != null && frame.fingerprint !== fingerprint) {
|
|
651
|
+
stale.push(id);
|
|
652
|
+
continue;
|
|
653
|
+
}
|
|
654
|
+
frames[id] = frame;
|
|
655
|
+
}
|
|
656
|
+
return { bundle: withFrames(bundle, frames), missing, stale };
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
/**
|
|
660
|
+
* Folds freshly captured frames into a stored bundle.
|
|
661
|
+
*
|
|
662
|
+
* Publishing per frame is what makes a capture proportional to what changed: one
|
|
663
|
+
* screen can be replaced without re-rendering the other seventy.
|
|
664
|
+
*/
|
|
665
|
+
export function mergeRoutePreviewArtifactV3(base, incoming) {
|
|
666
|
+
if (!isPlainRecord(incoming) || !isPlainRecord(incoming.frames)) {
|
|
667
|
+
throw new TypeError('Incoming route preview artifact is invalid.');
|
|
668
|
+
}
|
|
669
|
+
if (base == null) return withFrames(incoming, { ...incoming.frames });
|
|
670
|
+
if (!isPlainRecord(base) || !isPlainRecord(base.frames)) {
|
|
671
|
+
throw new TypeError('Stored route preview artifact is invalid.');
|
|
672
|
+
}
|
|
673
|
+
if (base.namespace !== incoming.namespace) {
|
|
674
|
+
throw new TypeError('Route preview artifacts belong to different namespaces.');
|
|
675
|
+
}
|
|
676
|
+
const frames = { ...base.frames, ...incoming.frames };
|
|
677
|
+
if (Object.keys(frames).length > ROUTE_PREVIEW_ARTIFACT_V3_LIMITS.frameCount) {
|
|
678
|
+
throw new RangeError('Route preview artifact has too many frames.');
|
|
679
|
+
}
|
|
680
|
+
const assets = {
|
|
681
|
+
head: { ...(base.assets?.head ?? {}), ...(incoming.assets?.head ?? {}) },
|
|
682
|
+
stylesheets: {
|
|
683
|
+
...(base.assets?.stylesheets ?? {}),
|
|
684
|
+
...(incoming.assets?.stylesheets ?? {}),
|
|
685
|
+
},
|
|
686
|
+
screenshots: {
|
|
687
|
+
...(base.assets?.screenshots ?? {}),
|
|
688
|
+
...(incoming.assets?.screenshots ?? {}),
|
|
689
|
+
},
|
|
690
|
+
};
|
|
691
|
+
return withFrames(
|
|
692
|
+
{
|
|
693
|
+
namespace: incoming.namespace,
|
|
694
|
+
// The newest capture names the bundle, but a frame keeps the fingerprint it
|
|
695
|
+
// was captured with, so the two no longer have to agree.
|
|
696
|
+
...(incoming.sourceRevision == null
|
|
697
|
+
? base.sourceRevision == null
|
|
698
|
+
? {}
|
|
699
|
+
: { sourceRevision: base.sourceRevision }
|
|
700
|
+
: { sourceRevision: incoming.sourceRevision }),
|
|
701
|
+
assets,
|
|
702
|
+
},
|
|
703
|
+
frames,
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
|
|
546
707
|
export function reconstructRoutePreviewArtifactSnapshotV3(bundle, frameId) {
|
|
547
708
|
const report = validateRoutePreviewArtifactV3(bundle);
|
|
548
709
|
if (!report.valid) {
|
package/package.json
CHANGED
package/storyboard.d.ts
CHANGED
|
@@ -225,6 +225,8 @@ export interface RoutePreviewArtifactV3Capture {
|
|
|
225
225
|
bytes: Uint8Array;
|
|
226
226
|
};
|
|
227
227
|
diagnostics?: readonly StoryboardCaptureDiagnostic[];
|
|
228
|
+
/** What this frame was captured from, compared per frame instead of per bundle. */
|
|
229
|
+
fingerprint?: string;
|
|
228
230
|
}
|
|
229
231
|
|
|
230
232
|
export interface RoutePreviewArtifactBundleV3 {
|
|
@@ -258,6 +260,22 @@ export declare function createRoutePreviewArtifactV3(input: {
|
|
|
258
260
|
sourceRevision?: string;
|
|
259
261
|
captures: Record<string, RoutePreviewArtifactV3Capture>;
|
|
260
262
|
}): RoutePreviewArtifactBundleV3;
|
|
263
|
+
export interface RoutePreviewArtifactFrameRequest {
|
|
264
|
+
id: string;
|
|
265
|
+
fingerprint?: string;
|
|
266
|
+
}
|
|
267
|
+
export declare function selectRoutePreviewArtifactFrames(
|
|
268
|
+
bundle: unknown,
|
|
269
|
+
wanted: readonly (RoutePreviewArtifactFrameRequest | string)[],
|
|
270
|
+
): {
|
|
271
|
+
bundle: RoutePreviewArtifactBundleV3;
|
|
272
|
+
missing: string[];
|
|
273
|
+
stale: string[];
|
|
274
|
+
};
|
|
275
|
+
export declare function mergeRoutePreviewArtifactV3(
|
|
276
|
+
base: unknown,
|
|
277
|
+
incoming: unknown,
|
|
278
|
+
): RoutePreviewArtifactBundleV3;
|
|
261
279
|
export declare function validateRoutePreviewArtifactV3(bundle: unknown): {
|
|
262
280
|
valid: boolean;
|
|
263
281
|
errors: string[];
|
package/types.d.ts
CHANGED
|
@@ -823,9 +823,16 @@ export interface PreviewArtifactExpectation {
|
|
|
823
823
|
sourceRevision: string;
|
|
824
824
|
}
|
|
825
825
|
|
|
826
|
+
export interface PreviewArtifactFrameRequest {
|
|
827
|
+
id: string;
|
|
828
|
+
fingerprint?: string;
|
|
829
|
+
}
|
|
830
|
+
|
|
826
831
|
export interface PreviewArtifactTransportRequest extends PreviewArtifactExpectation {
|
|
827
832
|
signal?: AbortSignal;
|
|
828
833
|
captureBaseUrl?: string;
|
|
834
|
+
/** Frames to fetch. Omitted asks for the whole bundle, as earlier versions did. */
|
|
835
|
+
frames?: readonly PreviewArtifactFrameRequest[];
|
|
829
836
|
}
|
|
830
837
|
|
|
831
838
|
export type PreviewArtifactTransport = (
|
|
@@ -889,6 +896,10 @@ export type PreviewArtifactBootstrapResult =
|
|
|
889
896
|
export declare function createPreviewRecipeFingerprint(
|
|
890
897
|
recipes: readonly PreviewRecipeIdentity[],
|
|
891
898
|
): string;
|
|
899
|
+
export declare function createPreviewFrameFingerprint(
|
|
900
|
+
screen: StoryboardCaptureRecipeInput & { id?: string },
|
|
901
|
+
options?: { dependencyDigest?: string },
|
|
902
|
+
): string;
|
|
892
903
|
export declare function createPreviewCacheNamespace(
|
|
893
904
|
options: PreviewCacheNamespaceOptions,
|
|
894
905
|
): string;
|
package/vite.d.ts
CHANGED
|
@@ -503,14 +503,17 @@ export declare function reconstructRoutePreviewArtifactSnapshots(
|
|
|
503
503
|
export {
|
|
504
504
|
ROUTE_PREVIEW_ARTIFACT_V3_LIMITS,
|
|
505
505
|
createRoutePreviewArtifactV3,
|
|
506
|
+
mergeRoutePreviewArtifactV3,
|
|
506
507
|
reconstructRoutePreviewArtifactScreenshotV3,
|
|
507
508
|
reconstructRoutePreviewArtifactSnapshotV3,
|
|
508
509
|
reconstructRoutePreviewArtifactSnapshotsV3,
|
|
510
|
+
selectRoutePreviewArtifactFrames,
|
|
509
511
|
validateRoutePreviewArtifactBundle,
|
|
510
512
|
validateRoutePreviewArtifactV3,
|
|
511
513
|
} from './storyboard';
|
|
512
514
|
export type {
|
|
513
515
|
RoutePreviewArtifactBundleV3,
|
|
516
|
+
RoutePreviewArtifactFrameRequest,
|
|
514
517
|
RoutePreviewArtifactV3Capture,
|
|
515
518
|
StoryboardCaptureDiagnostic,
|
|
516
519
|
StoryboardCaptureStatus,
|