@pygmalionjs/pygmalion 0.2.19 → 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/README.ko.md +19 -0
- package/README.md +19 -0
- package/dist-lib/{App-CiSnd2mV.js → App-B_6qmxhS.js} +2612 -2564
- package/dist-lib/pygmalion.js +870 -853
- package/dist-lib/style.css +1 -1
- package/dist-lib/testing.js +1 -1
- package/node/dev-mirror.mjs +228 -11
- package/node/preview-artifact-plugin.mjs +0 -0
- package/node/route-preview-artifact-v3.mjs +161 -0
- package/node/vite.mjs +9 -4
- package/package.json +1 -1
- package/storyboard.d.ts +18 -0
- package/types.d.ts +30 -0
- package/vite.d.ts +3 -0
|
@@ -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/node/vite.mjs
CHANGED
|
@@ -210,6 +210,10 @@ export function createPygmalionVitePlugins(config) {
|
|
|
210
210
|
}),
|
|
211
211
|
];
|
|
212
212
|
|
|
213
|
+
// Assigned below when the mirror plugin is built. The artifact plugin resolves
|
|
214
|
+
// it lazily because a capture starts long after the plugin list is assembled.
|
|
215
|
+
let devMirror = null;
|
|
216
|
+
|
|
213
217
|
if (project.preview.artifactFile || project.preview.generateArtifact) {
|
|
214
218
|
plugins.push(
|
|
215
219
|
pygmalionPreviewArtifactPlugin({
|
|
@@ -217,6 +221,7 @@ export function createPygmalionVitePlugins(config) {
|
|
|
217
221
|
artifactFile: project.preview.artifactFile,
|
|
218
222
|
endpoint: project.preview.artifactEndpoint,
|
|
219
223
|
generateArtifact: project.preview.generateArtifact,
|
|
224
|
+
acquireLease: (label) => devMirror?.acquireLease(label) ?? null,
|
|
220
225
|
}),
|
|
221
226
|
);
|
|
222
227
|
}
|
|
@@ -253,8 +258,7 @@ export function createPygmalionVitePlugins(config) {
|
|
|
253
258
|
}
|
|
254
259
|
|
|
255
260
|
if (project.mirror !== false) {
|
|
256
|
-
|
|
257
|
-
pygmalionDevMirrorPlugin({
|
|
261
|
+
const mirrorPlugin = pygmalionDevMirrorPlugin({
|
|
258
262
|
editorRoot: project.editorRoot,
|
|
259
263
|
projectRoot: project.projectRoot,
|
|
260
264
|
appDirectory: project.appDirectory,
|
|
@@ -280,8 +284,9 @@ export function createPygmalionVitePlugins(config) {
|
|
|
280
284
|
PYGMALION_DEV_CONTROL,
|
|
281
285
|
'mirror.control',
|
|
282
286
|
),
|
|
283
|
-
|
|
284
|
-
|
|
287
|
+
});
|
|
288
|
+
devMirror = mirrorPlugin.pygmalion ?? null;
|
|
289
|
+
plugins.push(mirrorPlugin);
|
|
285
290
|
}
|
|
286
291
|
|
|
287
292
|
if (project.sessions !== false) {
|
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
|
@@ -161,6 +161,17 @@ export interface TokenDef {
|
|
|
161
161
|
group?: string;
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
+
export interface ViewportPresetDef {
|
|
165
|
+
/** Button text. The panel column is narrow, so keep it to a few characters. */
|
|
166
|
+
label: string;
|
|
167
|
+
/** Frame width in CSS pixels. */
|
|
168
|
+
width: number;
|
|
169
|
+
/** Frame height. Omit to resize width only and leave the current height alone. */
|
|
170
|
+
height?: number;
|
|
171
|
+
/** Tooltip text. Defaults to the dimensions. */
|
|
172
|
+
description?: string;
|
|
173
|
+
}
|
|
174
|
+
|
|
164
175
|
export const DESIGN_IMPORT_KINDS: readonly [
|
|
165
176
|
'icons',
|
|
166
177
|
'design-tokens',
|
|
@@ -623,6 +634,12 @@ export declare function setComponentRegistry(r: ComponentRegistry): void;
|
|
|
623
634
|
|
|
624
635
|
export declare function setTokens(t: TokenDef[]): void;
|
|
625
636
|
|
|
637
|
+
/** Declares the frame widths the panel offers. A malformed entry throws at mount. */
|
|
638
|
+
export declare function setViewportPresets(
|
|
639
|
+
presets: readonly ViewportPresetDef[],
|
|
640
|
+
): void;
|
|
641
|
+
export declare function getViewportPresets(): ViewportPresetDef[];
|
|
642
|
+
|
|
626
643
|
/** Updates the standard route of the running route iframe. The existing editing status is maintained. */
|
|
627
644
|
export declare function setPygmalionAppOrigin(origin: string): void;
|
|
628
645
|
/** Update the static screen cache when a preview of the same URL points to a new code version. */
|
|
@@ -806,9 +823,16 @@ export interface PreviewArtifactExpectation {
|
|
|
806
823
|
sourceRevision: string;
|
|
807
824
|
}
|
|
808
825
|
|
|
826
|
+
export interface PreviewArtifactFrameRequest {
|
|
827
|
+
id: string;
|
|
828
|
+
fingerprint?: string;
|
|
829
|
+
}
|
|
830
|
+
|
|
809
831
|
export interface PreviewArtifactTransportRequest extends PreviewArtifactExpectation {
|
|
810
832
|
signal?: AbortSignal;
|
|
811
833
|
captureBaseUrl?: string;
|
|
834
|
+
/** Frames to fetch. Omitted asks for the whole bundle, as earlier versions did. */
|
|
835
|
+
frames?: readonly PreviewArtifactFrameRequest[];
|
|
812
836
|
}
|
|
813
837
|
|
|
814
838
|
export type PreviewArtifactTransport = (
|
|
@@ -872,6 +896,10 @@ export type PreviewArtifactBootstrapResult =
|
|
|
872
896
|
export declare function createPreviewRecipeFingerprint(
|
|
873
897
|
recipes: readonly PreviewRecipeIdentity[],
|
|
874
898
|
): string;
|
|
899
|
+
export declare function createPreviewFrameFingerprint(
|
|
900
|
+
screen: StoryboardCaptureRecipeInput & { id?: string },
|
|
901
|
+
options?: { dependencyDigest?: string },
|
|
902
|
+
): string;
|
|
875
903
|
export declare function createPreviewCacheNamespace(
|
|
876
904
|
options: PreviewCacheNamespaceOptions,
|
|
877
905
|
): string;
|
|
@@ -1620,6 +1648,8 @@ export declare function PygmalionEditor(props: {
|
|
|
1620
1648
|
registry?: ComponentRegistry;
|
|
1621
1649
|
tokens?: TokenDef[];
|
|
1622
1650
|
initialPages?: InitialPageDef[];
|
|
1651
|
+
/** Named frame widths the application supports — rendered as one-click buttons beside the W/H boxes. */
|
|
1652
|
+
viewportPresets?: readonly ViewportPresetDef[];
|
|
1623
1653
|
/** icons/tokens/atoms/components/screens Controller for getting inventory and full DOM layers. */
|
|
1624
1654
|
designImport?: DesignImportController;
|
|
1625
1655
|
/** Called with cumulative changes when editing asset panel tokens — Host is responsible for write-back (file saving). */
|
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,
|