@pygmalionjs/pygmalion 0.2.38 → 0.2.40

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.
@@ -0,0 +1,62 @@
1
+ function normalizedRoutePath(value) {
2
+ if (typeof value !== 'string' || !value.trim()) return null;
3
+ try {
4
+ const pathname = new URL(value.trim(), 'http://pygmalion.invalid').pathname;
5
+ return `/${pathname.split('/').filter(Boolean).join('/')}`;
6
+ } catch {
7
+ return null;
8
+ }
9
+ }
10
+
11
+ function matches(pattern, route) {
12
+ for (let index = 0; index < pattern.length; index += 1) {
13
+ const segment = pattern[index];
14
+ if (segment === '*' || (segment.startsWith(':') && segment.endsWith('*'))) {
15
+ return true;
16
+ }
17
+ if (route[index] == null) return false;
18
+ if (!segment.startsWith(':') && segment !== route[index]) return false;
19
+ }
20
+ return route.length === pattern.length;
21
+ }
22
+
23
+ /** Resolves a concrete route against exact, `:param`, and `*` manifest paths. */
24
+ export function resolvePreviewRouteDependencyDigest(route, entries) {
25
+ const normalized = normalizedRoutePath(route);
26
+ if (!normalized || !Array.isArray(entries)) return undefined;
27
+ const routeSegments = normalized.split('/').filter(Boolean);
28
+ const candidates = [];
29
+ for (const [order, entry] of entries.entries()) {
30
+ if (
31
+ typeof entry?.path !== 'string' ||
32
+ typeof entry?.dependencyDigest !== 'string' ||
33
+ !entry.dependencyDigest
34
+ ) {
35
+ continue;
36
+ }
37
+ const pattern = normalizedRoutePath(entry.path);
38
+ if (!pattern) continue;
39
+ if (pattern === normalized) return entry.dependencyDigest;
40
+ const segments = pattern.split('/').filter(Boolean);
41
+ if (!matches(segments, routeSegments)) continue;
42
+ candidates.push({
43
+ digest: entry.dependencyDigest,
44
+ literalCount: segments.filter(
45
+ (segment) => !segment.startsWith(':') && segment !== '*',
46
+ ).length,
47
+ wildcardCount: segments.filter(
48
+ (segment) => segment === '*' || segment.endsWith('*'),
49
+ ).length,
50
+ length: segments.length,
51
+ order,
52
+ });
53
+ }
54
+ candidates.sort(
55
+ (left, right) =>
56
+ right.literalCount - left.literalCount ||
57
+ left.wildcardCount - right.wildcardCount ||
58
+ right.length - left.length ||
59
+ right.order - left.order,
60
+ );
61
+ return candidates[0]?.digest;
62
+ }
@@ -398,9 +398,9 @@ export function createRoutePreviewArtifactV3(input) {
398
398
  {
399
399
  status: rawCapture.status,
400
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.
401
+ // Content identity within the source revision that captured this frame.
402
+ // Store selections preserve the revision as per-frame provenance when
403
+ // frames from separate revisions share one response bundle.
404
404
  ...(rawCapture.fingerprint == null
405
405
  ? {}
406
406
  : { fingerprint: String(rawCapture.fingerprint) }),
@@ -490,6 +490,10 @@ export function validateRoutePreviewArtifactV3(bundle) {
490
490
  errors.push(`Frame "${frameId}" fingerprint is invalid.`);
491
491
  continue;
492
492
  }
493
+ if (!validOptionalIdentifier(rawFrame.sourceRevision)) {
494
+ errors.push(`Frame "${frameId}" source revision is invalid.`);
495
+ continue;
496
+ }
493
497
  const diagnostics = rawFrame.diagnostics;
494
498
  if (
495
499
  !Array.isArray(diagnostics) ||
@@ -622,11 +626,11 @@ function withFrames(bundle, frames) {
622
626
  /**
623
627
  * Picks the frames a consumer asked for and reports what it must still capture.
624
628
  *
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.
629
+ * A frame is usable when it exists and its stored revision and fingerprint match
630
+ * the requested provenance. Callers that omit either constraint retain the
631
+ * looser compatibility behavior of earlier versions.
628
632
  */
629
- export function selectRoutePreviewArtifactFrames(bundle, wanted) {
633
+ export function selectRoutePreviewArtifactFrames(bundle, wanted, options = {}) {
630
634
  if (!isPlainRecord(bundle) || !isPlainRecord(bundle.frames)) {
631
635
  throw new TypeError('Route preview artifact is invalid.');
632
636
  }
@@ -647,8 +651,14 @@ export function selectRoutePreviewArtifactFrames(bundle, wanted) {
647
651
  continue;
648
652
  }
649
653
  const fingerprint = isPlainRecord(request) ? request.fingerprint : undefined;
650
- if (fingerprint != null && frame.fingerprint !== fingerprint) {
654
+ const frameSourceRevision = frame.sourceRevision ?? bundle.sourceRevision;
655
+ if (
656
+ (options.sourceRevision != null &&
657
+ frameSourceRevision !== options.sourceRevision) ||
658
+ (fingerprint != null && frame.fingerprint !== fingerprint)
659
+ ) {
651
660
  stale.push(id);
661
+ if (options.includeStale === true) frames[id] = frame;
652
662
  continue;
653
663
  }
654
664
  frames[id] = frame;
@@ -673,7 +683,23 @@ export function mergeRoutePreviewArtifactV3(base, incoming) {
673
683
  if (base.namespace !== incoming.namespace) {
674
684
  throw new TypeError('Route preview artifacts belong to different namespaces.');
675
685
  }
676
- const frames = { ...base.frames, ...incoming.frames };
686
+ const mixedRevisions = base.sourceRevision !== incoming.sourceRevision;
687
+ const preserveRevision = (frames, sourceRevision) =>
688
+ Object.fromEntries(
689
+ Object.entries(frames).map(([id, frame]) => [
690
+ id,
691
+ mixedRevisions &&
692
+ isPlainRecord(frame) &&
693
+ frame.sourceRevision == null &&
694
+ sourceRevision != null
695
+ ? { ...frame, sourceRevision }
696
+ : frame,
697
+ ]),
698
+ );
699
+ const frames = {
700
+ ...preserveRevision(base.frames, base.sourceRevision),
701
+ ...preserveRevision(incoming.frames, incoming.sourceRevision),
702
+ };
677
703
  if (Object.keys(frames).length > ROUTE_PREVIEW_ARTIFACT_V3_LIMITS.frameCount) {
678
704
  throw new RangeError('Route preview artifact has too many frames.');
679
705
  }
@@ -691,13 +717,15 @@ export function mergeRoutePreviewArtifactV3(base, incoming) {
691
717
  return withFrames(
692
718
  {
693
719
  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 }),
720
+ // A mixed bundle has no honest bundle-wide revision. Its frames carry
721
+ // their own provenance instead, including known revisions on both sides.
722
+ ...(mixedRevisions
723
+ ? {}
724
+ : incoming.sourceRevision == null
725
+ ? base.sourceRevision == null
726
+ ? {}
727
+ : { sourceRevision: base.sourceRevision }
728
+ : { sourceRevision: incoming.sourceRevision }),
701
729
  assets,
702
730
  },
703
731
  frames,
@@ -42,9 +42,11 @@ export {
42
42
  export {
43
43
  ROUTE_PREVIEW_ARTIFACT_V3_LIMITS,
44
44
  createRoutePreviewArtifactV3,
45
+ mergeRoutePreviewArtifactV3,
45
46
  reconstructRoutePreviewArtifactScreenshotV3,
46
47
  reconstructRoutePreviewArtifactSnapshotV3,
47
48
  reconstructRoutePreviewArtifactSnapshotsV3,
49
+ selectRoutePreviewArtifactFrames,
48
50
  validateRoutePreviewArtifactBundle,
49
51
  validateRoutePreviewArtifactV3,
50
52
  } from './route-preview-artifact-v3.mjs';
package/node/vite.mjs CHANGED
@@ -55,12 +55,15 @@ import {
55
55
  import {
56
56
  ROUTE_PREVIEW_ARTIFACT_V3_LIMITS,
57
57
  createRoutePreviewArtifactV3,
58
+ mergeRoutePreviewArtifactV3,
58
59
  reconstructRoutePreviewArtifactScreenshotV3,
59
60
  reconstructRoutePreviewArtifactSnapshotV3,
60
61
  reconstructRoutePreviewArtifactSnapshotsV3,
62
+ selectRoutePreviewArtifactFrames,
61
63
  validateRoutePreviewArtifactBundle,
62
64
  validateRoutePreviewArtifactV3,
63
65
  } from './route-preview-artifact-v3.mjs';
66
+ import { resolvePreviewRouteDependencyDigest } from './route-dependency-digest.mjs';
64
67
  import {
65
68
  DEFAULT_PYGMALION_PREVIEW_ARTIFACT_FILE,
66
69
  PYGMALION_PREVIEW_ARTIFACT_ENDPOINT,
@@ -366,9 +369,12 @@ export {
366
369
  validateRoutePreviewArtifactV2,
367
370
  ROUTE_PREVIEW_ARTIFACT_V3_LIMITS,
368
371
  createRoutePreviewArtifactV3,
372
+ mergeRoutePreviewArtifactV3,
369
373
  reconstructRoutePreviewArtifactScreenshotV3,
370
374
  reconstructRoutePreviewArtifactSnapshotV3,
371
375
  reconstructRoutePreviewArtifactSnapshotsV3,
376
+ selectRoutePreviewArtifactFrames,
377
+ resolvePreviewRouteDependencyDigest,
372
378
  validateRoutePreviewArtifactBundle,
373
379
  validateRoutePreviewArtifactV3,
374
380
  DEFAULT_PYGMALION_PREVIEW_ARTIFACT_FILE,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pygmalionjs/pygmalion",
3
- "version": "0.2.38",
3
+ "version": "0.2.40",
4
4
  "description": "Code-backed DOM design sandbox and visual QA editor",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {
@@ -37,9 +37,12 @@
37
37
  "node/dev-view.vite.mjs",
38
38
  "node/inspect-plugin.mjs",
39
39
  "node/preview-artifact-plugin.mjs",
40
+ "node/preview-artifact-store.mjs",
41
+ "node/preview-vite-cache.mjs",
40
42
  "node/qa-capture-plugin.mjs",
41
43
  "node/route-preview-artifact-v2.mjs",
42
44
  "node/route-preview-artifact-v3.mjs",
45
+ "node/route-dependency-digest.mjs",
43
46
  "node/source-revision.mjs",
44
47
  "node/source-diff.mjs",
45
48
  "node/source-graph.mjs",
package/storyboard.d.ts CHANGED
@@ -318,6 +318,7 @@ export interface RoutePreviewArtifactFrameRequest {
318
318
  export declare function selectRoutePreviewArtifactFrames(
319
319
  bundle: unknown,
320
320
  wanted: readonly (RoutePreviewArtifactFrameRequest | string)[],
321
+ options?: { includeStale?: boolean; sourceRevision?: string },
321
322
  ): {
322
323
  bundle: RoutePreviewArtifactBundleV3;
323
324
  missing: string[];
package/types.d.ts CHANGED
@@ -994,6 +994,8 @@ export interface RoutePreviewArtifactV3ScreenshotAsset {
994
994
  export interface RoutePreviewArtifactV3Frame {
995
995
  readonly status: RoutePreviewArtifactV3Status;
996
996
  readonly viewport: RoutePreviewArtifactV3Viewport;
997
+ readonly sourceRevision?: string;
998
+ readonly fingerprint?: string;
997
999
  readonly snapshot?: RoutePreviewArtifactBundleV2['frames'][string];
998
1000
  readonly screenshot?: RoutePreviewArtifactV3ScreenshotReference;
999
1001
  readonly diagnostics: readonly RoutePreviewArtifactV3Diagnostic[];
@@ -1945,6 +1947,8 @@ export declare function PygmalionEditor(props: {
1945
1947
  registry?: ComponentRegistry;
1946
1948
  tokens?: TokenDef[];
1947
1949
  initialPages?: InitialPageDef[];
1950
+ /** Canvas shown on first mount. Useful when an earlier catalog canvas is expensive to render. */
1951
+ initialCanvas?: string;
1948
1952
  /** Explicit external-design to registered-code component mappings. */
1949
1953
  componentConnections?: readonly DesignComponentConnectionDef[];
1950
1954
  /** Named frame widths the application supports — rendered as one-click buttons beside the W/H boxes. */
package/vite.d.ts CHANGED
@@ -76,6 +76,7 @@ export interface PygmalionPreviewConfig {
76
76
  generateArtifact?: (request: {
77
77
  namespace: string;
78
78
  sourceRevision: string;
79
+ frames?: readonly { id: string; fingerprint?: string }[];
79
80
  captureBaseUrl?: string;
80
81
  }) => unknown | Promise<unknown>;
81
82
  }
@@ -158,8 +159,15 @@ export declare function pygmalionPreviewArtifactPlugin(options?: {
158
159
  generateArtifact?: (request: {
159
160
  namespace: string;
160
161
  sourceRevision: string;
162
+ frames?: readonly { id: string; fingerprint?: string }[];
161
163
  captureBaseUrl?: string;
162
164
  }) => unknown | Promise<unknown>;
165
+ acquireLease?: (
166
+ reason: string,
167
+ ) =>
168
+ | { release(): void | Promise<void> }
169
+ | null
170
+ | Promise<{ release(): void | Promise<void> } | null>;
163
171
  }): PluginOption;
164
172
  export interface PygmalionGitSourceState {
165
173
  commit: string;
@@ -511,6 +519,10 @@ export {
511
519
  validateRoutePreviewArtifactBundle,
512
520
  validateRoutePreviewArtifactV3,
513
521
  } from './storyboard';
522
+ export declare function resolvePreviewRouteDependencyDigest(
523
+ route: string,
524
+ entries: readonly { path?: string; dependencyDigest?: string }[],
525
+ ): string | undefined;
514
526
  export type {
515
527
  RoutePreviewArtifactBundleV3,
516
528
  RoutePreviewArtifactFrameRequest,