@usefragments/core 1.0.0
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/LICENSE +21 -0
- package/dist/chunk-243QYRUF.js +2145 -0
- package/dist/chunk-243QYRUF.js.map +1 -0
- package/dist/chunk-3LLRNCPX.js +111 -0
- package/dist/chunk-3LLRNCPX.js.map +1 -0
- package/dist/chunk-EIYNNS77.js +48 -0
- package/dist/chunk-EIYNNS77.js.map +1 -0
- package/dist/chunk-JNBFJ34I.js +326 -0
- package/dist/chunk-JNBFJ34I.js.map +1 -0
- package/dist/chunk-R3X7UBHU.js +228 -0
- package/dist/chunk-R3X7UBHU.js.map +1 -0
- package/dist/chunk-SH4KPIYH.js +393 -0
- package/dist/chunk-SH4KPIYH.js.map +1 -0
- package/dist/chunk-V4VQB57N.js +56 -0
- package/dist/chunk-V4VQB57N.js.map +1 -0
- package/dist/chunk-X34IA4LR.js +319 -0
- package/dist/chunk-X34IA4LR.js.map +1 -0
- package/dist/chunk-YF65VYRY.js +493 -0
- package/dist/chunk-YF65VYRY.js.map +1 -0
- package/dist/codes/index.d.ts +40 -0
- package/dist/codes/index.js +23 -0
- package/dist/codes/index.js.map +1 -0
- package/dist/compiled-types/index.d.ts +5 -0
- package/dist/compiled-types/index.js +1 -0
- package/dist/compiled-types/index.js.map +1 -0
- package/dist/generate/index.d.ts +47 -0
- package/dist/generate/index.js +11 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/governance-BOa3KyiJ.d.ts +3279 -0
- package/dist/graph/index.d.ts +66 -0
- package/dist/graph/index.js +17 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/index.d.ts +11388 -0
- package/dist/index.js +8711 -0
- package/dist/index.js.map +1 -0
- package/dist/package-identity.d.ts +42 -0
- package/dist/package-identity.js +23 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/preview/index.d.ts +484 -0
- package/dist/preview/index.js +1129 -0
- package/dist/preview/index.js.map +1 -0
- package/dist/react-types.d.ts +22 -0
- package/dist/react-types.js +1 -0
- package/dist/react-types.js.map +1 -0
- package/dist/registry.d.ts +2367 -0
- package/dist/registry.js +62 -0
- package/dist/registry.js.map +1 -0
- package/dist/schemas/index.d.ts +2584 -0
- package/dist/schemas/index.js +55 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/severity.d.ts +18 -0
- package/dist/severity.js +27 -0
- package/dist/severity.js.map +1 -0
- package/dist/test-utils.d.ts +14 -0
- package/dist/test-utils.js +63 -0
- package/dist/test-utils.js.map +1 -0
- package/dist/topology/index.d.ts +48 -0
- package/dist/topology/index.js +7 -0
- package/dist/topology/index.js.map +1 -0
- package/dist/types-xJ2xyp_G.d.ts +119 -0
- package/package.json +120 -0
- package/src/__tests__/contract-parser.test.ts +614 -0
- package/src/__tests__/dtcg-generators.test.ts +439 -0
- package/src/__tests__/dtcg-parser.test.ts +884 -0
- package/src/__tests__/preview-runtime.test.tsx +111 -0
- package/src/__tests__/schema.test.ts +31 -0
- package/src/__tests__/severity.test.ts +34 -0
- package/src/__tests__/style-comparison.test.ts +662 -0
- package/src/__tests__/tokens-compat.test.ts +51 -0
- package/src/agent-format.test.ts +105 -0
- package/src/agent-format.ts +141 -0
- package/src/bundle.ts +149 -0
- package/src/codes/__tests__/codes.test.ts +73 -0
- package/src/codes/codes.ts +463 -0
- package/src/codes/index.ts +7 -0
- package/src/codes/source-finding-bridge.ts +113 -0
- package/src/compiled-types/index.ts +293 -0
- package/src/component-contract.ts +231 -0
- package/src/component-discovery.ts +80 -0
- package/src/composition.test.ts +262 -0
- package/src/composition.ts +318 -0
- package/src/config-paths.test.ts +78 -0
- package/src/config-paths.ts +47 -0
- package/src/config.ts +18 -0
- package/src/conform-prove.test.ts +274 -0
- package/src/conform-prove.ts +326 -0
- package/src/conform.ts +160 -0
- package/src/constants.ts +114 -0
- package/src/context.ts +2 -0
- package/src/contract/fixture.ts +110 -0
- package/src/contract/hash.test.ts +86 -0
- package/src/contract/hash.ts +195 -0
- package/src/contract/index.ts +43 -0
- package/src/contract/preimage.test.ts +254 -0
- package/src/contract/preimage.ts +365 -0
- package/src/contract/stamp.test.ts +100 -0
- package/src/contract/stamp.ts +105 -0
- package/src/contract-parser.ts +141 -0
- package/src/defineFragment.test.ts +257 -0
- package/src/defineFragment.ts +293 -0
- package/src/dtcg-generators.ts +324 -0
- package/src/dtcg-parser.ts +1 -0
- package/src/dtcg.ts +237 -0
- package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
- package/src/facts/builders.ts +835 -0
- package/src/facts/compile.ts +422 -0
- package/src/facts/fact-index.ts +394 -0
- package/src/facts/facts.test.ts +494 -0
- package/src/facts/ids.ts +125 -0
- package/src/facts/index.ts +119 -0
- package/src/facts/types.ts +614 -0
- package/src/figma.ts +263 -0
- package/src/fragment-types.ts +214 -0
- package/src/generate/generate.test.ts +594 -0
- package/src/generate/index.ts +438 -0
- package/src/governance-integrity.test.ts +186 -0
- package/src/governance-integrity.ts +282 -0
- package/src/governance.test.ts +300 -0
- package/src/governance.ts +605 -0
- package/src/governance.type-test.ts +75 -0
- package/src/graph/engine.ts +446 -0
- package/src/graph/graph.test.ts +746 -0
- package/src/graph/index.ts +32 -0
- package/src/graph/serialization.ts +55 -0
- package/src/graph/types.ts +178 -0
- package/src/index.ts +891 -0
- package/src/local-canonical.ts +102 -0
- package/src/mcp-snapshot.ts +222 -0
- package/src/package-identity-match.ts +33 -0
- package/src/package-identity.generated.ts +233 -0
- package/src/package-identity.test.ts +99 -0
- package/src/package-identity.ts +157 -0
- package/src/performance-presets.ts +142 -0
- package/src/preview/comparison-artifact.test.ts +228 -0
- package/src/preview/comparison-artifact.ts +181 -0
- package/src/preview/comparison-context.ts +93 -0
- package/src/preview/comparison-evidence.ts +112 -0
- package/src/preview/comparison-format.ts +21 -0
- package/src/preview/comparison-presentation.ts +187 -0
- package/src/preview/comparison-references.test.ts +430 -0
- package/src/preview/comparison-references.ts +72 -0
- package/src/preview/comparison-score.ts +59 -0
- package/src/preview/comparison-styles.ts +197 -0
- package/src/preview/comparison.test.ts +559 -0
- package/src/preview/comparison.ts +488 -0
- package/src/preview/index.ts +10 -0
- package/src/preview/types.ts +309 -0
- package/src/preview/validation.test.ts +290 -0
- package/src/preview/validation.ts +356 -0
- package/src/preview-runtime.tsx +144 -0
- package/src/raw-html-canonical.test.ts +41 -0
- package/src/raw-html-canonical.ts +179 -0
- package/src/react-types.ts +21 -0
- package/src/registry-install-plan.ts +344 -0
- package/src/registry.test.ts +245 -0
- package/src/registry.ts +369 -0
- package/src/rules/a11y-required-accessible-name.ts +93 -0
- package/src/rules/color-math.test.ts +72 -0
- package/src/rules/color-math.ts +192 -0
- package/src/rules/components-forbidden-prop-value.ts +108 -0
- package/src/rules/components-prefer-library.ts +943 -0
- package/src/rules/components-unknown-prop.ts +94 -0
- package/src/rules/composition-pattern-schema.ts +125 -0
- package/src/rules/composition-pattern.test.ts +320 -0
- package/src/rules/composition-pattern.ts +271 -0
- package/src/rules/emit-gate.test.ts +42 -0
- package/src/rules/emit-gate.ts +60 -0
- package/src/rules/finding.ts +54 -0
- package/src/rules/index.ts +235 -0
- package/src/rules/jsx-preferred-component.ts +68 -0
- package/src/rules/jsx-preferred-import-path.ts +67 -0
- package/src/rules/presets.ts +57 -0
- package/src/rules/props-invalid-value.ts +115 -0
- package/src/rules/rules.test.ts +3636 -0
- package/src/rules/spacing-resolution.ts +184 -0
- package/src/rules/styles-no-raw-color.ts +329 -0
- package/src/rules/styles-no-raw-dimensions.ts +357 -0
- package/src/rules/styles-no-raw-spacing.ts +236 -0
- package/src/rules/styles-no-raw-typography.ts +237 -0
- package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
- package/src/rules/tailwind-arbitrary-color.ts +93 -0
- package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
- package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
- package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
- package/src/rules/tailwind-forbidden-palette.ts +107 -0
- package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
- package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
- package/src/rules/tailwind-raw-color-via-token.ts +82 -0
- package/src/rules/tailwind-rule-utils.ts +106 -0
- package/src/rules/tailwind-unknown-class.test.ts +157 -0
- package/src/rules/tailwind-unknown-class.ts +121 -0
- package/src/rules/taxonomy.test.ts +238 -0
- package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
- package/src/rules/tiers.ts +111 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
- package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
- package/src/rules/tokens-require-dual-fallback.ts +78 -0
- package/src/rules/types.ts +27 -0
- package/src/rules/utils.test.ts +110 -0
- package/src/rules/utils.ts +599 -0
- package/src/schema.ts +427 -0
- package/src/schemas/index.ts +275 -0
- package/src/schemas/schemas.test.ts +124 -0
- package/src/severity.ts +54 -0
- package/src/storyAdapter.test.ts +571 -0
- package/src/storyAdapter.ts +761 -0
- package/src/storyFilters.test.ts +350 -0
- package/src/storyFilters.ts +253 -0
- package/src/storybook-csf.ts +11 -0
- package/src/style-comparison.ts +423 -0
- package/src/test-utils.ts +68 -0
- package/src/token-parser.ts +2 -0
- package/src/token-types.ts +287 -0
- package/src/tokens/categories.ts +131 -0
- package/src/tokens/color.ts +193 -0
- package/src/tokens/design-token-parser.test.ts +159 -0
- package/src/tokens/design-token-parser.ts +402 -0
- package/src/tokens/dtcg.ts +428 -0
- package/src/tokens/index.ts +130 -0
- package/src/tokens/lookup.ts +19 -0
- package/src/tokens/scss.ts +171 -0
- package/src/tokens/tailwind-v4.ts +187 -0
- package/src/tokens/types.ts +43 -0
- package/src/topology/index.ts +9 -0
- package/src/topology/resolve-area.test.ts +174 -0
- package/src/topology/resolve-area.ts +215 -0
- package/src/types.ts +1057 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OWNED_PACKAGE_AUTHORING_EPOCH,
|
|
3
|
+
OWNED_PACKAGE_EXPLICIT_POLICIES,
|
|
4
|
+
OWNED_PACKAGE_IDENTITY_SOURCE_SHA256,
|
|
5
|
+
OWNED_PACKAGE_ROOT_POLICIES,
|
|
6
|
+
} from "./package-identity.generated.js";
|
|
7
|
+
|
|
8
|
+
export type OwnedPackageStatus = "retained" | "moved" | "retired";
|
|
9
|
+
|
|
10
|
+
export interface OwnedPackageIdentityResolution {
|
|
11
|
+
readonly input: string;
|
|
12
|
+
readonly owned: boolean;
|
|
13
|
+
readonly identityId?: string;
|
|
14
|
+
readonly status?: OwnedPackageStatus;
|
|
15
|
+
/** Supported public read target. Absent for retired identities. */
|
|
16
|
+
readonly canonicalSpecifier?: string;
|
|
17
|
+
/** What first-party generators write in the active authoring epoch. */
|
|
18
|
+
readonly authoringSpecifier?: string;
|
|
19
|
+
/** Permanent legacy spelling used only inside the fcid-preimage:v1 domain. */
|
|
20
|
+
readonly v1Specifier: string;
|
|
21
|
+
/** Supported public spellings for this exact logical read identity. */
|
|
22
|
+
readonly readEquivalents?: readonly string[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
type ExplicitPolicy = (typeof OWNED_PACKAGE_EXPLICIT_POLICIES)[number];
|
|
26
|
+
type RootPolicy = (typeof OWNED_PACKAGE_ROOT_POLICIES)[number];
|
|
27
|
+
|
|
28
|
+
function appendSubpath(root: string | null, suffix: string): string | undefined {
|
|
29
|
+
if (!root) return undefined;
|
|
30
|
+
return suffix ? `${root}/${suffix}` : root;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function rootSuffix(specifier: string, root: string): string | null {
|
|
34
|
+
if (specifier === root) return "";
|
|
35
|
+
const prefix = `${root}/`;
|
|
36
|
+
return specifier.startsWith(prefix) ? specifier.slice(prefix.length) : null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function authoringValue(policy: {
|
|
40
|
+
readonly authoring: { readonly legacy: string; readonly current: string | null };
|
|
41
|
+
}): string | undefined {
|
|
42
|
+
return policy.authoring[OWNED_PACKAGE_AUTHORING_EPOCH] ?? undefined;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function resolveExplicit(specifier: string): OwnedPackageIdentityResolution | undefined {
|
|
46
|
+
for (const policy of OWNED_PACKAGE_EXPLICIT_POLICIES) {
|
|
47
|
+
if (specifier !== policy.legacySpecifier && specifier !== policy.currentSpecifier) continue;
|
|
48
|
+
|
|
49
|
+
// A target can also be the canonical identity of another package. The
|
|
50
|
+
// historical spelling is always unambiguous. Current targets participate
|
|
51
|
+
// here only when the policy explicitly owns the v1 projection (Preview ->
|
|
52
|
+
// Core/preview); CLI/core deliberately stays distinct from Core root.
|
|
53
|
+
if (specifier === policy.currentSpecifier && !policy.matchCurrentForV1) continue;
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
input: specifier,
|
|
57
|
+
owned: true,
|
|
58
|
+
identityId: policy.id,
|
|
59
|
+
status: policy.status,
|
|
60
|
+
canonicalSpecifier: policy.currentSpecifier ?? undefined,
|
|
61
|
+
authoringSpecifier: authoringValue(policy),
|
|
62
|
+
v1Specifier: policy.v1Projection,
|
|
63
|
+
readEquivalents: policy.readEquivalents,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resolveRoot(specifier: string): OwnedPackageIdentityResolution | undefined {
|
|
70
|
+
for (const policy of OWNED_PACKAGE_ROOT_POLICIES) {
|
|
71
|
+
const legacySuffix = rootSuffix(specifier, policy.legacyRoot);
|
|
72
|
+
const currentSuffix = policy.currentRoot ? rootSuffix(specifier, policy.currentRoot) : null;
|
|
73
|
+
const suffix = legacySuffix ?? currentSuffix;
|
|
74
|
+
if (suffix === null) continue;
|
|
75
|
+
|
|
76
|
+
const canonicalSpecifier =
|
|
77
|
+
policy.status === "retained"
|
|
78
|
+
? appendSubpath(policy.currentRoot, suffix)
|
|
79
|
+
: suffix === ""
|
|
80
|
+
? (policy.targetSpecifier ?? undefined)
|
|
81
|
+
: undefined;
|
|
82
|
+
const authoredRoot = authoringValue(policy);
|
|
83
|
+
|
|
84
|
+
return {
|
|
85
|
+
input: specifier,
|
|
86
|
+
owned: true,
|
|
87
|
+
identityId: policy.id,
|
|
88
|
+
status: policy.status,
|
|
89
|
+
canonicalSpecifier,
|
|
90
|
+
authoringSpecifier: appendSubpath(authoredRoot ?? null, suffix),
|
|
91
|
+
v1Specifier: appendSubpath(policy.v1Projection, suffix) ?? specifier,
|
|
92
|
+
readEquivalents: policy.readEquivalents.map((root) => appendSubpath(root, suffix) ?? root),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return undefined;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Resolve only exact Fragments-owned npm roots/subpaths. Unknown packages and
|
|
100
|
+
* arbitrary strings are intentionally opaque; this is not a substring rewrite.
|
|
101
|
+
*/
|
|
102
|
+
export function resolveOwnedPackageImport(specifier: string): OwnedPackageIdentityResolution {
|
|
103
|
+
return (
|
|
104
|
+
resolveExplicit(specifier) ??
|
|
105
|
+
resolveRoot(specifier) ?? {
|
|
106
|
+
input: specifier,
|
|
107
|
+
owned: false,
|
|
108
|
+
v1Specifier: specifier,
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Canonical current-read spelling. Retired/unknown identities are returned
|
|
115
|
+
* unchanged so a caller cannot accidentally expose a private @repo/* target.
|
|
116
|
+
*/
|
|
117
|
+
export function canonicalizeOwnedImport(specifier: string): string {
|
|
118
|
+
return resolveOwnedPackageImport(specifier).canonicalSpecifier ?? specifier;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Active-epoch spelling for generators; retired/unknown inputs stay unchanged. */
|
|
122
|
+
export function authorOwnedImport(specifier: string): string {
|
|
123
|
+
return resolveOwnedPackageImport(specifier).authoringSpecifier ?? specifier;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Permanent compatibility encoder for fcid-preimage:v1 only. Never use this
|
|
128
|
+
* result for source edits, suggestions, provenance, or generated guidance.
|
|
129
|
+
*/
|
|
130
|
+
export function projectV1OwnedImportIdentity(specifier: string): string {
|
|
131
|
+
return resolveOwnedPackageImport(specifier).v1Specifier;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function ownedImportEquivalents(specifier: string): readonly string[] {
|
|
135
|
+
return resolveOwnedPackageImport(specifier).readEquivalents ?? [specifier];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function mapComponentId(value: string, mapPackage: (packageName: string) => string): string {
|
|
139
|
+
const separator = value.lastIndexOf("#");
|
|
140
|
+
if (separator <= 0 || separator === value.length - 1) return value;
|
|
141
|
+
const packageName = value.slice(0, separator);
|
|
142
|
+
const componentName = value.slice(separator + 1);
|
|
143
|
+
return `${mapPackage(packageName)}#${componentName}`;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export function canonicalizeOwnedComponentId(value: string): string {
|
|
147
|
+
return mapComponentId(value, canonicalizeOwnedImport);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** fcid-preimage:v1 component-id compatibility; subject to the same restriction. */
|
|
151
|
+
export function projectV1OwnedComponentId(value: string): string {
|
|
152
|
+
return mapComponentId(value, projectV1OwnedImportIdentity);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** Exposed for diagnostics and generator tests, not as a second policy owner. */
|
|
156
|
+
export const OWNED_PACKAGE_IDENTITY_EPOCH = OWNED_PACKAGE_AUTHORING_EPOCH;
|
|
157
|
+
export { OWNED_PACKAGE_IDENTITY_SOURCE_SHA256 };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance budget presets and classification utilities.
|
|
3
|
+
*
|
|
4
|
+
* ESLint model: global defaults, zero-config, auto-measurement.
|
|
5
|
+
* Users configure 0-3 numbers. Per-component overrides are the `eslint-disable` equivalent.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Types
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export interface PerformanceBudgets {
|
|
13
|
+
/** Maximum gzipped bundle size in bytes */
|
|
14
|
+
bundleSize: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PerformanceConfig {
|
|
18
|
+
preset: string;
|
|
19
|
+
budgets: PerformanceBudgets;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export type ComplexityTier = 'lightweight' | 'moderate' | 'heavy';
|
|
23
|
+
|
|
24
|
+
export interface PerformanceData {
|
|
25
|
+
/** Gzipped bundle size in bytes */
|
|
26
|
+
bundleSize: number;
|
|
27
|
+
/** Raw (minified, not gzipped) bundle size in bytes */
|
|
28
|
+
rawSize: number;
|
|
29
|
+
/** Complexity classification */
|
|
30
|
+
complexity: ComplexityTier;
|
|
31
|
+
/** Percentage of budget used (0-100+) */
|
|
32
|
+
budgetPercent: number;
|
|
33
|
+
/** Whether the component exceeds its budget */
|
|
34
|
+
overBudget: boolean;
|
|
35
|
+
/** ISO timestamp when measured */
|
|
36
|
+
measuredAt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface PerformanceSummary {
|
|
40
|
+
/** Preset name used */
|
|
41
|
+
preset: string;
|
|
42
|
+
/** Budget applied in bytes */
|
|
43
|
+
budget: number;
|
|
44
|
+
/** Total components measured */
|
|
45
|
+
total: number;
|
|
46
|
+
/** Number of components over budget */
|
|
47
|
+
overBudget: number;
|
|
48
|
+
/** Distribution by tier */
|
|
49
|
+
tiers: Record<ComplexityTier, number>;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Presets
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
const PRESETS: Record<string, PerformanceBudgets> = {
|
|
57
|
+
strict: { bundleSize: 8 * 1024 }, // 8KB gzipped
|
|
58
|
+
standard: { bundleSize: 15 * 1024 }, // 15KB gzipped
|
|
59
|
+
relaxed: { bundleSize: 30 * 1024 }, // 30KB gzipped
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export const PRESET_NAMES = Object.keys(PRESETS) as readonly string[];
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Resolution
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve a performance config from user input.
|
|
70
|
+
* Accepts a preset name string or a custom config object.
|
|
71
|
+
*/
|
|
72
|
+
export function resolvePerformanceConfig(
|
|
73
|
+
input: string | { preset?: string; budgets?: Partial<PerformanceBudgets> } | undefined
|
|
74
|
+
): PerformanceConfig {
|
|
75
|
+
if (!input) {
|
|
76
|
+
return { preset: 'standard', budgets: PRESETS.standard };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (typeof input === 'string') {
|
|
80
|
+
const budgets = PRESETS[input];
|
|
81
|
+
if (!budgets) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`Unknown performance preset "${input}". Available: ${PRESET_NAMES.join(', ')}`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
return { preset: input, budgets };
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const presetName = input.preset ?? 'standard';
|
|
90
|
+
const baseBudgets = PRESETS[presetName];
|
|
91
|
+
if (!baseBudgets) {
|
|
92
|
+
throw new Error(
|
|
93
|
+
`Unknown performance preset "${presetName}". Available: ${PRESET_NAMES.join(', ')}`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
preset: presetName,
|
|
99
|
+
budgets: {
|
|
100
|
+
bundleSize: input.budgets?.bundleSize ?? baseBudgets.bundleSize,
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------
|
|
106
|
+
// Classification
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Classify a component's complexity based on gzipped bundle size.
|
|
111
|
+
*
|
|
112
|
+
* - lightweight: < 5KB — simple, leaf components
|
|
113
|
+
* - moderate: < 15KB — typical composed components
|
|
114
|
+
* - heavy: >= 15KB — complex widgets with dependencies
|
|
115
|
+
*/
|
|
116
|
+
export function classifyComplexity(gzipBytes: number): ComplexityTier {
|
|
117
|
+
if (gzipBytes < 5 * 1024) return 'lightweight';
|
|
118
|
+
if (gzipBytes < 15 * 1024) return 'moderate';
|
|
119
|
+
return 'heavy';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
// Formatting helpers
|
|
124
|
+
// ---------------------------------------------------------------------------
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Format bytes to a human-readable string (e.g. "2.1KB", "15.3KB").
|
|
128
|
+
*/
|
|
129
|
+
export function formatBytes(bytes: number): string {
|
|
130
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
131
|
+
const kb = bytes / 1024;
|
|
132
|
+
return kb < 10 ? `${kb.toFixed(1)}KB` : `${Math.round(kb)}KB`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Create a visual budget bar for terminal output.
|
|
137
|
+
*/
|
|
138
|
+
export function budgetBar(percent: number, width = 20): string {
|
|
139
|
+
const filled = Math.min(Math.round((percent / 100) * width), width);
|
|
140
|
+
const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
|
|
141
|
+
return percent > 100 ? `\x1b[31m${bar}\x1b[0m` : `\x1b[32m${bar}\x1b[0m`;
|
|
142
|
+
}
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
applyPreviewComparisonArtifactPayload,
|
|
4
|
+
buildPreviewComparisonArtifactPayload,
|
|
5
|
+
comparePreviewToDesign,
|
|
6
|
+
type PreviewArtifactManifest,
|
|
7
|
+
} from "./index";
|
|
8
|
+
|
|
9
|
+
describe("preview comparison artifact payloads", () => {
|
|
10
|
+
it("applies reusable evidence to a variant design reference", () => {
|
|
11
|
+
const manifest = buildManifest();
|
|
12
|
+
const report = comparePreviewToDesign({
|
|
13
|
+
design: {
|
|
14
|
+
kind: "figma",
|
|
15
|
+
label: "Button / Primary",
|
|
16
|
+
imageDataUrl: "data:image/png;base64,figma",
|
|
17
|
+
size: { width: 120, height: 40 },
|
|
18
|
+
},
|
|
19
|
+
preview: {
|
|
20
|
+
kind: "preview",
|
|
21
|
+
label: "Button / Primary",
|
|
22
|
+
componentKey: "Button",
|
|
23
|
+
variantId: "primary",
|
|
24
|
+
theme: "light",
|
|
25
|
+
viewport: { width: 120, height: 40 },
|
|
26
|
+
imageDataUrl: "data:image/png;base64,preview",
|
|
27
|
+
size: { width: 120, height: 40 },
|
|
28
|
+
},
|
|
29
|
+
imageDiff: {
|
|
30
|
+
diffPercentage: 0.2,
|
|
31
|
+
diffImageDataUrl: "data:image/png;base64,diff",
|
|
32
|
+
},
|
|
33
|
+
});
|
|
34
|
+
const payload = buildPreviewComparisonArtifactPayload({ report });
|
|
35
|
+
|
|
36
|
+
const result = applyPreviewComparisonArtifactPayload(manifest, payload!);
|
|
37
|
+
|
|
38
|
+
expect(result.status).toBe("applied");
|
|
39
|
+
expect(result.applied).toBe(true);
|
|
40
|
+
expect(result.manifest).not.toBe(manifest);
|
|
41
|
+
expect(manifest.components.Button.variants[0]?.design).toBeUndefined();
|
|
42
|
+
expect(result.manifest.components.Button.design).toBeUndefined();
|
|
43
|
+
expect(result.manifest.components.Button.variants[0]?.design).toMatchObject({
|
|
44
|
+
label: "Button / Primary",
|
|
45
|
+
comparison: {
|
|
46
|
+
previewImageUrl: "data:image/png;base64,preview",
|
|
47
|
+
diffImageUrl: "data:image/png;base64,diff",
|
|
48
|
+
diffPercentage: 0.2,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("applies component-scoped evidence to the component design reference", () => {
|
|
54
|
+
const manifest = buildManifest();
|
|
55
|
+
const report = comparePreviewToDesign({
|
|
56
|
+
design: {
|
|
57
|
+
kind: "figma",
|
|
58
|
+
label: "Button / Default",
|
|
59
|
+
imageDataUrl: "data:image/png;base64,figma",
|
|
60
|
+
size: { width: 120, height: 40 },
|
|
61
|
+
},
|
|
62
|
+
preview: {
|
|
63
|
+
kind: "preview",
|
|
64
|
+
label: "Button / Default",
|
|
65
|
+
componentKey: "Button",
|
|
66
|
+
variantId: "primary",
|
|
67
|
+
theme: "light",
|
|
68
|
+
viewport: { width: 120, height: 40 },
|
|
69
|
+
imageDataUrl: "data:image/png;base64,preview",
|
|
70
|
+
size: { width: 120, height: 40 },
|
|
71
|
+
},
|
|
72
|
+
imageDiff: {
|
|
73
|
+
diffPercentage: 0.2,
|
|
74
|
+
},
|
|
75
|
+
context: {
|
|
76
|
+
scope: "component",
|
|
77
|
+
active: {
|
|
78
|
+
variantId: "primary",
|
|
79
|
+
theme: "light",
|
|
80
|
+
viewport: { width: 120, height: 40 },
|
|
81
|
+
},
|
|
82
|
+
stale: false,
|
|
83
|
+
staleReasons: [],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
const payload = buildPreviewComparisonArtifactPayload({ report });
|
|
87
|
+
|
|
88
|
+
const result = applyPreviewComparisonArtifactPayload(manifest, payload!);
|
|
89
|
+
|
|
90
|
+
expect(result.status).toBe("applied");
|
|
91
|
+
expect(result.manifest.components.Button.design).toMatchObject({
|
|
92
|
+
label: "Button / Default",
|
|
93
|
+
comparison: {
|
|
94
|
+
diffPercentage: 0.2,
|
|
95
|
+
},
|
|
96
|
+
});
|
|
97
|
+
expect(result.manifest.components.Button.variants[0]?.design).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("refuses to apply stale reports with no reusable evidence", () => {
|
|
101
|
+
const manifest = buildManifest();
|
|
102
|
+
const report = comparePreviewToDesign({
|
|
103
|
+
design: {
|
|
104
|
+
kind: "figma",
|
|
105
|
+
label: "Button",
|
|
106
|
+
size: { width: 120, height: 40 },
|
|
107
|
+
},
|
|
108
|
+
preview: {
|
|
109
|
+
kind: "preview",
|
|
110
|
+
label: "Button",
|
|
111
|
+
componentKey: "Button",
|
|
112
|
+
size: { width: 120, height: 40 },
|
|
113
|
+
},
|
|
114
|
+
context: {
|
|
115
|
+
scope: "component",
|
|
116
|
+
captured: { theme: "light" },
|
|
117
|
+
active: { theme: "dark" },
|
|
118
|
+
stale: true,
|
|
119
|
+
staleReasons: ["theme"],
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
const payload = buildPreviewComparisonArtifactPayload({ report });
|
|
123
|
+
|
|
124
|
+
const result = applyPreviewComparisonArtifactPayload(manifest, payload!);
|
|
125
|
+
|
|
126
|
+
expect(result).toMatchObject({
|
|
127
|
+
status: "missing-comparison-evidence",
|
|
128
|
+
applied: false,
|
|
129
|
+
manifest,
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("reports missing target components and variants without mutating the manifest", () => {
|
|
134
|
+
const manifest = buildManifest();
|
|
135
|
+
const report = comparePreviewToDesign({
|
|
136
|
+
design: { kind: "figma", label: "Missing", size: { width: 120, height: 40 } },
|
|
137
|
+
preview: {
|
|
138
|
+
kind: "preview",
|
|
139
|
+
label: "Missing",
|
|
140
|
+
componentKey: "Missing",
|
|
141
|
+
variantId: "primary",
|
|
142
|
+
size: { width: 120, height: 40 },
|
|
143
|
+
},
|
|
144
|
+
imageDiff: { diffPercentage: 0.2 },
|
|
145
|
+
});
|
|
146
|
+
const payload = buildPreviewComparisonArtifactPayload({ report });
|
|
147
|
+
|
|
148
|
+
expect(applyPreviewComparisonArtifactPayload(manifest, payload!)).toMatchObject({
|
|
149
|
+
status: "component-not-found",
|
|
150
|
+
applied: false,
|
|
151
|
+
manifest,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
applyPreviewComparisonArtifactPayload(manifest, {
|
|
156
|
+
...payload!,
|
|
157
|
+
target: { ...payload!.target, componentKey: "Button", variantId: "missing" },
|
|
158
|
+
})
|
|
159
|
+
).toMatchObject({
|
|
160
|
+
status: "variant-not-found",
|
|
161
|
+
applied: false,
|
|
162
|
+
manifest,
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
function buildManifest(): PreviewArtifactManifest {
|
|
168
|
+
return {
|
|
169
|
+
artifactVersion: "preview-artifact/v1",
|
|
170
|
+
identity: {
|
|
171
|
+
artifactId: "artifact",
|
|
172
|
+
artifactVersion: "preview-artifact/v1",
|
|
173
|
+
orgId: "org",
|
|
174
|
+
projectId: "project",
|
|
175
|
+
bindingId: "binding",
|
|
176
|
+
repoFullName: "acme/ui",
|
|
177
|
+
commitSha: "abc123",
|
|
178
|
+
sourceRef: "main",
|
|
179
|
+
createdAt: "2026-05-24T00:00:00.000Z",
|
|
180
|
+
},
|
|
181
|
+
builder: {
|
|
182
|
+
name: "test",
|
|
183
|
+
version: "1.0.0",
|
|
184
|
+
typecheck: "none",
|
|
185
|
+
sources: ["manual"],
|
|
186
|
+
adapters: [],
|
|
187
|
+
},
|
|
188
|
+
runtime: {
|
|
189
|
+
framework: "react",
|
|
190
|
+
moduleFormat: "esm",
|
|
191
|
+
entry: "index.js",
|
|
192
|
+
css: [],
|
|
193
|
+
assetsBaseUrl: "/preview/",
|
|
194
|
+
},
|
|
195
|
+
capabilities: {
|
|
196
|
+
supportsThemes: true,
|
|
197
|
+
supportsViewports: true,
|
|
198
|
+
supportsActions: false,
|
|
199
|
+
supportsSlots: false,
|
|
200
|
+
supportsA11yScan: false,
|
|
201
|
+
supportsPlayFunctions: false,
|
|
202
|
+
supportsSceneNodes: false,
|
|
203
|
+
},
|
|
204
|
+
components: {
|
|
205
|
+
Button: {
|
|
206
|
+
componentKey: "Button",
|
|
207
|
+
publicRef: "Button",
|
|
208
|
+
displayName: "Button",
|
|
209
|
+
exportName: "Button",
|
|
210
|
+
importPath: "./button",
|
|
211
|
+
tier: "primitive",
|
|
212
|
+
previewability: "ready",
|
|
213
|
+
definitionSource: "manual",
|
|
214
|
+
diagnostics: [],
|
|
215
|
+
props: {},
|
|
216
|
+
variants: [
|
|
217
|
+
{
|
|
218
|
+
id: "primary",
|
|
219
|
+
name: "Primary",
|
|
220
|
+
source: "manual",
|
|
221
|
+
props: {},
|
|
222
|
+
},
|
|
223
|
+
],
|
|
224
|
+
},
|
|
225
|
+
},
|
|
226
|
+
diagnostics: [],
|
|
227
|
+
};
|
|
228
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
PreviewArtifactManifest,
|
|
3
|
+
PreviewDesignComparisonReference,
|
|
4
|
+
PreviewDesignReference,
|
|
5
|
+
} from "./types";
|
|
6
|
+
import {
|
|
7
|
+
buildPreviewDesignComparisonEvidence,
|
|
8
|
+
buildPreviewDesignReferenceFromReport,
|
|
9
|
+
type PreviewComparisonReferenceScope,
|
|
10
|
+
type PreviewComparisonReport,
|
|
11
|
+
} from "./comparison";
|
|
12
|
+
|
|
13
|
+
export interface PreviewComparisonArtifactAssetPaths {
|
|
14
|
+
rendered?: string;
|
|
15
|
+
figma?: string;
|
|
16
|
+
diff?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface PreviewComparisonArtifactTarget {
|
|
20
|
+
componentKey: string;
|
|
21
|
+
variantId?: string;
|
|
22
|
+
designReferenceScope: PreviewComparisonReferenceScope;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface BuildPreviewComparisonArtifactPayloadInput {
|
|
26
|
+
report?: PreviewComparisonReport;
|
|
27
|
+
designReference?: PreviewDesignReference;
|
|
28
|
+
comparisonEvidence?: PreviewDesignComparisonReference;
|
|
29
|
+
assets?: PreviewComparisonArtifactAssetPaths;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PreviewComparisonArtifactPayload {
|
|
33
|
+
target: PreviewComparisonArtifactTarget;
|
|
34
|
+
report: PreviewComparisonReport;
|
|
35
|
+
designReference: PreviewDesignReference;
|
|
36
|
+
comparisonEvidence?: PreviewDesignComparisonReference;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export type ApplyPreviewComparisonArtifactStatus =
|
|
40
|
+
| "applied"
|
|
41
|
+
| "component-not-found"
|
|
42
|
+
| "variant-not-found"
|
|
43
|
+
| "missing-comparison-evidence";
|
|
44
|
+
|
|
45
|
+
export interface ApplyPreviewComparisonArtifactPayloadResult {
|
|
46
|
+
status: ApplyPreviewComparisonArtifactStatus;
|
|
47
|
+
applied: boolean;
|
|
48
|
+
manifest: PreviewArtifactManifest;
|
|
49
|
+
message: string;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function buildPreviewComparisonArtifactPayload(
|
|
53
|
+
input: BuildPreviewComparisonArtifactPayloadInput
|
|
54
|
+
): PreviewComparisonArtifactPayload | null {
|
|
55
|
+
if (!input.report) return null;
|
|
56
|
+
|
|
57
|
+
const report = input.report;
|
|
58
|
+
const designReference: PreviewDesignReference = {
|
|
59
|
+
...(input.designReference ?? buildPreviewDesignReferenceFromReport(report)),
|
|
60
|
+
};
|
|
61
|
+
const reportEvidence = buildPreviewDesignComparisonEvidence(report);
|
|
62
|
+
const comparisonEvidence: PreviewDesignComparisonReference | undefined = report.context?.stale
|
|
63
|
+
? undefined
|
|
64
|
+
: input.comparisonEvidence
|
|
65
|
+
? { ...input.comparisonEvidence }
|
|
66
|
+
: designReference.comparison
|
|
67
|
+
? { ...designReference.comparison }
|
|
68
|
+
: reportEvidence
|
|
69
|
+
? { ...reportEvidence }
|
|
70
|
+
: undefined;
|
|
71
|
+
|
|
72
|
+
if (report.context?.stale) {
|
|
73
|
+
delete designReference.comparison;
|
|
74
|
+
}
|
|
75
|
+
if (input.assets?.figma) {
|
|
76
|
+
designReference.imageUrl = input.assets.figma;
|
|
77
|
+
}
|
|
78
|
+
if (comparisonEvidence && input.assets?.rendered) {
|
|
79
|
+
comparisonEvidence.previewImageUrl = input.assets.rendered;
|
|
80
|
+
}
|
|
81
|
+
if (comparisonEvidence && input.assets?.diff) {
|
|
82
|
+
comparisonEvidence.diffImageUrl = input.assets.diff;
|
|
83
|
+
}
|
|
84
|
+
if (comparisonEvidence) {
|
|
85
|
+
designReference.comparison = comparisonEvidence;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
target: buildComparisonArtifactTarget(report),
|
|
90
|
+
report,
|
|
91
|
+
designReference,
|
|
92
|
+
...(comparisonEvidence && { comparisonEvidence }),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function applyPreviewComparisonArtifactPayload(
|
|
97
|
+
manifest: PreviewArtifactManifest,
|
|
98
|
+
payload: PreviewComparisonArtifactPayload
|
|
99
|
+
): ApplyPreviewComparisonArtifactPayloadResult {
|
|
100
|
+
if (!payload.comparisonEvidence) {
|
|
101
|
+
return {
|
|
102
|
+
status: "missing-comparison-evidence",
|
|
103
|
+
applied: false,
|
|
104
|
+
manifest,
|
|
105
|
+
message: "Comparison payload has no reusable evidence to apply.",
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const component = manifest.components[payload.target.componentKey];
|
|
110
|
+
if (!component) {
|
|
111
|
+
return {
|
|
112
|
+
status: "component-not-found",
|
|
113
|
+
applied: false,
|
|
114
|
+
manifest,
|
|
115
|
+
message: `Component ${payload.target.componentKey} was not found in the manifest.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (payload.target.designReferenceScope === "variant") {
|
|
120
|
+
const variantIndex = component.variants.findIndex(
|
|
121
|
+
(variant) => variant.id === payload.target.variantId
|
|
122
|
+
);
|
|
123
|
+
if (variantIndex < 0) {
|
|
124
|
+
return {
|
|
125
|
+
status: "variant-not-found",
|
|
126
|
+
applied: false,
|
|
127
|
+
manifest,
|
|
128
|
+
message: `Variant ${payload.target.variantId ?? "(missing)"} was not found on component ${
|
|
129
|
+
payload.target.componentKey
|
|
130
|
+
}.`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
const variants = component.variants.map((variant, index) =>
|
|
135
|
+
index === variantIndex ? { ...variant, design: payload.designReference } : variant
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
status: "applied",
|
|
140
|
+
applied: true,
|
|
141
|
+
manifest: {
|
|
142
|
+
...manifest,
|
|
143
|
+
components: {
|
|
144
|
+
...manifest.components,
|
|
145
|
+
[payload.target.componentKey]: {
|
|
146
|
+
...component,
|
|
147
|
+
variants,
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
},
|
|
151
|
+
message: `Applied comparison evidence to ${payload.target.componentKey} variant ${payload.target.variantId}.`,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
status: "applied",
|
|
157
|
+
applied: true,
|
|
158
|
+
manifest: {
|
|
159
|
+
...manifest,
|
|
160
|
+
components: {
|
|
161
|
+
...manifest.components,
|
|
162
|
+
[payload.target.componentKey]: {
|
|
163
|
+
...component,
|
|
164
|
+
design: payload.designReference,
|
|
165
|
+
},
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
message: `Applied comparison evidence to ${payload.target.componentKey}.`,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function buildComparisonArtifactTarget(
|
|
173
|
+
report: PreviewComparisonReport
|
|
174
|
+
): PreviewComparisonArtifactTarget {
|
|
175
|
+
return {
|
|
176
|
+
componentKey: report.preview.componentKey,
|
|
177
|
+
...(report.preview.variantId && { variantId: report.preview.variantId }),
|
|
178
|
+
designReferenceScope:
|
|
179
|
+
report.context?.scope ?? (report.preview.variantId ? "variant" : "component"),
|
|
180
|
+
};
|
|
181
|
+
}
|