@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,356 @@
|
|
|
1
|
+
import type { PreviewArtifactManifest } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface PreviewArtifactValidationResult {
|
|
4
|
+
ok: boolean;
|
|
5
|
+
issues: string[];
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export function definePreviewArtifactManifest(
|
|
9
|
+
manifest: PreviewArtifactManifest
|
|
10
|
+
): PreviewArtifactManifest {
|
|
11
|
+
const validation = validatePreviewArtifactManifest(manifest);
|
|
12
|
+
if (!validation.ok) {
|
|
13
|
+
throw new Error(`Invalid preview artifact manifest:\n${validation.issues.join("\n")}`);
|
|
14
|
+
}
|
|
15
|
+
return manifest;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validatePreviewArtifactManifest(value: unknown): PreviewArtifactValidationResult {
|
|
19
|
+
const issues: string[] = [];
|
|
20
|
+
if (!isRecord(value)) {
|
|
21
|
+
return { ok: false, issues: ["manifest must be an object"] };
|
|
22
|
+
}
|
|
23
|
+
requireLiteral(value, "artifactVersion", "preview-artifact/v1", issues);
|
|
24
|
+
validateIdentity(value.identity, issues);
|
|
25
|
+
validateBuilder(value.builder, issues);
|
|
26
|
+
validateRuntime(value.runtime, issues);
|
|
27
|
+
validateComponents(value.components, issues);
|
|
28
|
+
if (!Array.isArray(value.diagnostics)) {
|
|
29
|
+
issues.push("diagnostics must be an array");
|
|
30
|
+
}
|
|
31
|
+
return { ok: issues.length === 0, issues };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function isPreviewArtifactManifest(value: unknown): value is PreviewArtifactManifest {
|
|
35
|
+
return validatePreviewArtifactManifest(value).ok;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function validateIdentity(value: unknown, issues: string[]) {
|
|
39
|
+
if (!isRecord(value)) {
|
|
40
|
+
issues.push("identity must be an object");
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
requireLiteral(value, "artifactVersion", "preview-artifact/v1", issues);
|
|
44
|
+
for (const key of [
|
|
45
|
+
"artifactId",
|
|
46
|
+
"orgId",
|
|
47
|
+
"projectId",
|
|
48
|
+
"bindingId",
|
|
49
|
+
"repoFullName",
|
|
50
|
+
"commitSha",
|
|
51
|
+
"sourceRef",
|
|
52
|
+
"createdAt",
|
|
53
|
+
]) {
|
|
54
|
+
requireString(value, key, issues);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function validateBuilder(value: unknown, issues: string[]) {
|
|
59
|
+
if (!isRecord(value)) {
|
|
60
|
+
issues.push("builder must be an object");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
requireString(value, "name", issues);
|
|
64
|
+
requireString(value, "version", issues);
|
|
65
|
+
if (value.typecheck !== "typescript" && value.typecheck !== "none") {
|
|
66
|
+
issues.push('builder.typecheck must be "typescript" or "none"');
|
|
67
|
+
}
|
|
68
|
+
if (!Array.isArray(value.sources)) {
|
|
69
|
+
issues.push("builder.sources must be an array");
|
|
70
|
+
} else {
|
|
71
|
+
for (const source of value.sources) {
|
|
72
|
+
if (!isPreviewDefinitionSource(source)) {
|
|
73
|
+
issues.push(`builder.sources contains unsupported source ${JSON.stringify(source)}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
if (!Array.isArray(value.adapters)) {
|
|
78
|
+
issues.push("builder.adapters must be an array");
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function validateRuntime(value: unknown, issues: string[]) {
|
|
83
|
+
if (!isRecord(value)) {
|
|
84
|
+
issues.push("runtime must be an object");
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
requireLiteral(value, "framework", "react", issues);
|
|
88
|
+
requireLiteral(value, "moduleFormat", "esm", issues);
|
|
89
|
+
requireString(value, "entry", issues);
|
|
90
|
+
requireString(value, "assetsBaseUrl", issues);
|
|
91
|
+
if (!Array.isArray(value.css)) issues.push("runtime.css must be an array");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function validateComponents(value: unknown, issues: string[]) {
|
|
95
|
+
if (!isRecord(value)) {
|
|
96
|
+
issues.push("components must be an object");
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const [key, component] of Object.entries(value)) {
|
|
100
|
+
if (!isRecord(component)) {
|
|
101
|
+
issues.push(`components.${key} must be an object`);
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
requireString(component, "componentKey", issues, `components.${key}`);
|
|
105
|
+
requireString(component, "publicRef", issues, `components.${key}`);
|
|
106
|
+
requireString(component, "displayName", issues, `components.${key}`);
|
|
107
|
+
requireString(component, "exportName", issues, `components.${key}`);
|
|
108
|
+
requireString(component, "importPath", issues, `components.${key}`);
|
|
109
|
+
requireString(component, "definitionSource", issues, `components.${key}`);
|
|
110
|
+
if (!isPreviewDefinitionSource(component.definitionSource)) {
|
|
111
|
+
issues.push(
|
|
112
|
+
`components.${key}.definitionSource must be one of typescript, fragment, manual, synthesized`
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
if (!isRecord(component.props)) {
|
|
116
|
+
issues.push(`components.${key}.props must be an object`);
|
|
117
|
+
}
|
|
118
|
+
if (!Array.isArray(component.variants)) {
|
|
119
|
+
issues.push(`components.${key}.variants must be an array`);
|
|
120
|
+
} else {
|
|
121
|
+
component.variants.forEach((variant, index) => {
|
|
122
|
+
if (!isRecord(variant)) {
|
|
123
|
+
issues.push(`components.${key}.variants.${index} must be an object`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
validateDesignReference(
|
|
127
|
+
variant.design,
|
|
128
|
+
issues,
|
|
129
|
+
`components.${key}.variants.${index}.design`
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
validateDesignReference(component.design, issues, `components.${key}.design`);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function validateDesignReference(value: unknown, issues: string[], path: string) {
|
|
138
|
+
if (value === undefined) return;
|
|
139
|
+
if (!isRecord(value)) {
|
|
140
|
+
issues.push(`${path} must be an object`);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
if (value.provider !== "figma") {
|
|
144
|
+
issues.push(`${path}.provider must be "figma"`);
|
|
145
|
+
}
|
|
146
|
+
for (const key of ["label", "nodeUrl", "fileKey", "nodeId", "imageUrl"]) {
|
|
147
|
+
if (value[key] !== undefined && typeof value[key] !== "string") {
|
|
148
|
+
issues.push(`${path}.${key} must be a string`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
validateSize(value.size, issues, `${path}.size`);
|
|
152
|
+
validateComparisonEvidence(value.comparison, issues, `${path}.comparison`, readSize(value.size));
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function validateComparisonEvidence(
|
|
156
|
+
value: unknown,
|
|
157
|
+
issues: string[],
|
|
158
|
+
path: string,
|
|
159
|
+
designSize: { width: number; height: number } | undefined
|
|
160
|
+
) {
|
|
161
|
+
if (value === undefined) return;
|
|
162
|
+
if (!isRecord(value)) {
|
|
163
|
+
issues.push(`${path} must be an object`);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
for (const key of ["previewImageUrl", "diffImageUrl", "capturedAt"]) {
|
|
168
|
+
if (value[key] !== undefined && typeof value[key] !== "string") {
|
|
169
|
+
issues.push(`${path}.${key} must be a string`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
validateSize(value.previewSize, issues, `${path}.previewSize`);
|
|
174
|
+
validateComparisonState(value.state, issues, `${path}.state`);
|
|
175
|
+
const comparisonSize = readSize(value.previewSize) ?? designSize;
|
|
176
|
+
validateStyleMap(value.designStyles, issues, `${path}.designStyles`);
|
|
177
|
+
validateStyleMap(value.previewStyles, issues, `${path}.previewStyles`);
|
|
178
|
+
|
|
179
|
+
validateFiniteNumber(value.diffPercentage, issues, `${path}.diffPercentage`);
|
|
180
|
+
validatePixelCounts(value, issues, path);
|
|
181
|
+
|
|
182
|
+
if (
|
|
183
|
+
typeof value.diffPercentage === "number" &&
|
|
184
|
+
(value.diffPercentage < 0 || value.diffPercentage > 100)
|
|
185
|
+
) {
|
|
186
|
+
issues.push(`${path}.diffPercentage must be between 0 and 100`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (value.changedRegions !== undefined) {
|
|
190
|
+
if (!Array.isArray(value.changedRegions)) {
|
|
191
|
+
issues.push(`${path}.changedRegions must be an array`);
|
|
192
|
+
} else {
|
|
193
|
+
value.changedRegions.forEach((region, index) =>
|
|
194
|
+
validateBounds(region, issues, `${path}.changedRegions.${index}`, comparisonSize)
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function validateComparisonState(value: unknown, issues: string[], path: string) {
|
|
201
|
+
if (value === undefined) return;
|
|
202
|
+
if (!isRecord(value)) {
|
|
203
|
+
issues.push(`${path} must be an object`);
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
if (value.variantId !== undefined && typeof value.variantId !== "string") {
|
|
207
|
+
issues.push(`${path}.variantId must be a string`);
|
|
208
|
+
}
|
|
209
|
+
if (value.theme !== undefined && typeof value.theme !== "string") {
|
|
210
|
+
issues.push(`${path}.theme must be a string`);
|
|
211
|
+
}
|
|
212
|
+
validateSize(value.viewport, issues, `${path}.viewport`);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function validatePixelCounts(value: Record<string, unknown>, issues: string[], path: string) {
|
|
216
|
+
if (value.diffPixelCount !== undefined) {
|
|
217
|
+
validateNonNegativeInteger(value.diffPixelCount, issues, `${path}.diffPixelCount`);
|
|
218
|
+
}
|
|
219
|
+
if (value.totalPixels !== undefined) {
|
|
220
|
+
validatePositiveInteger(value.totalPixels, issues, `${path}.totalPixels`);
|
|
221
|
+
}
|
|
222
|
+
if (
|
|
223
|
+
isFiniteNumber(value.diffPixelCount) &&
|
|
224
|
+
isFiniteNumber(value.totalPixels) &&
|
|
225
|
+
value.diffPixelCount > value.totalPixels
|
|
226
|
+
) {
|
|
227
|
+
issues.push(`${path}.diffPixelCount must be less than or equal to ${path}.totalPixels`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function validateSize(value: unknown, issues: string[], path: string) {
|
|
232
|
+
if (value === undefined) return;
|
|
233
|
+
if (!isRecord(value)) {
|
|
234
|
+
issues.push(`${path} must be an object`);
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
validatePositiveNumber(value.width, issues, `${path}.width`);
|
|
238
|
+
validatePositiveNumber(value.height, issues, `${path}.height`);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function validateStyleMap(value: unknown, issues: string[], path: string) {
|
|
242
|
+
if (value === undefined) return;
|
|
243
|
+
if (!isRecord(value)) {
|
|
244
|
+
issues.push(`${path} must be an object`);
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
for (const [key, styleValue] of Object.entries(value)) {
|
|
248
|
+
if (typeof styleValue !== "string") {
|
|
249
|
+
issues.push(`${path}.${key} must be a string`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function validateBounds(
|
|
255
|
+
value: unknown,
|
|
256
|
+
issues: string[],
|
|
257
|
+
path: string,
|
|
258
|
+
sourceSize?: { width: number; height: number }
|
|
259
|
+
) {
|
|
260
|
+
if (!isRecord(value)) {
|
|
261
|
+
issues.push(`${path} must be an object`);
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
validateNonNegativeNumber(value.x, issues, `${path}.x`);
|
|
265
|
+
validateNonNegativeNumber(value.y, issues, `${path}.y`);
|
|
266
|
+
validatePositiveNumber(value.width, issues, `${path}.width`);
|
|
267
|
+
validatePositiveNumber(value.height, issues, `${path}.height`);
|
|
268
|
+
|
|
269
|
+
if (!sourceSize) return;
|
|
270
|
+
if (
|
|
271
|
+
isFiniteNumber(value.x) &&
|
|
272
|
+
isFiniteNumber(value.width) &&
|
|
273
|
+
value.x >= 0 &&
|
|
274
|
+
value.width > 0 &&
|
|
275
|
+
value.x + value.width > sourceSize.width
|
|
276
|
+
) {
|
|
277
|
+
issues.push(`${path} must fit within comparison source width ${sourceSize.width}`);
|
|
278
|
+
}
|
|
279
|
+
if (
|
|
280
|
+
isFiniteNumber(value.y) &&
|
|
281
|
+
isFiniteNumber(value.height) &&
|
|
282
|
+
value.y >= 0 &&
|
|
283
|
+
value.height > 0 &&
|
|
284
|
+
value.y + value.height > sourceSize.height
|
|
285
|
+
) {
|
|
286
|
+
issues.push(`${path} must fit within comparison source height ${sourceSize.height}`);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
291
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function isPreviewDefinitionSource(value: unknown): boolean {
|
|
295
|
+
return (
|
|
296
|
+
value === "typescript" || value === "fragment" || value === "manual" || value === "synthesized"
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
301
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function validatePositiveNumber(value: unknown, issues: string[], path: string) {
|
|
305
|
+
if (!isFiniteNumber(value) || value <= 0) {
|
|
306
|
+
issues.push(`${path} must be a positive number`);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
function validateNonNegativeNumber(value: unknown, issues: string[], path: string) {
|
|
311
|
+
if (!isFiniteNumber(value) || value < 0) {
|
|
312
|
+
issues.push(`${path} must be a non-negative number`);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
function validateFiniteNumber(value: unknown, issues: string[], path: string) {
|
|
317
|
+
if (value !== undefined && !isFiniteNumber(value)) {
|
|
318
|
+
issues.push(`${path} must be a finite number`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function validatePositiveInteger(value: unknown, issues: string[], path: string) {
|
|
323
|
+
if (!Number.isInteger(value) || !isFiniteNumber(value) || value <= 0) {
|
|
324
|
+
issues.push(`${path} must be a positive integer`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function validateNonNegativeInteger(value: unknown, issues: string[], path: string) {
|
|
329
|
+
if (!Number.isInteger(value) || !isFiniteNumber(value) || value < 0) {
|
|
330
|
+
issues.push(`${path} must be a non-negative integer`);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function readSize(value: unknown): { width: number; height: number } | undefined {
|
|
335
|
+
if (!isRecord(value)) return undefined;
|
|
336
|
+
if (!isFiniteNumber(value.width) || value.width <= 0) return undefined;
|
|
337
|
+
if (!isFiniteNumber(value.height) || value.height <= 0) return undefined;
|
|
338
|
+
return { width: value.width, height: value.height };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function requireString(object: Record<string, unknown>, key: string, issues: string[], path = "") {
|
|
342
|
+
if (typeof object[key] !== "string" || !object[key]) {
|
|
343
|
+
issues.push(`${path ? `${path}.` : ""}${key} must be a non-empty string`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function requireLiteral(
|
|
348
|
+
object: Record<string, unknown>,
|
|
349
|
+
key: string,
|
|
350
|
+
expected: string,
|
|
351
|
+
issues: string[]
|
|
352
|
+
) {
|
|
353
|
+
if (object[key] !== expected) {
|
|
354
|
+
issues.push(`${key} must be ${JSON.stringify(expected)}`);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
2
|
+
import type { VariantLoader, VariantRenderOptions } from "./types.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Minimal contract for rendering a preview variant.
|
|
6
|
+
* Compatible with fragment variants and Storybook-adapted variants.
|
|
7
|
+
*/
|
|
8
|
+
export interface PreviewVariantLike {
|
|
9
|
+
render: (options?: VariantRenderOptions) => ReactNode;
|
|
10
|
+
loaders?: VariantLoader[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PreviewRuntimeState {
|
|
14
|
+
content: ReactNode | null;
|
|
15
|
+
isLoading: boolean;
|
|
16
|
+
error: Error | null;
|
|
17
|
+
loadedData?: Record<string, unknown>;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface PreviewRuntimeOptions {
|
|
21
|
+
variant?: PreviewVariantLike | null;
|
|
22
|
+
loadedData?: Record<string, unknown>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const EMPTY_STATE: PreviewRuntimeState = {
|
|
26
|
+
content: null,
|
|
27
|
+
isLoading: false,
|
|
28
|
+
error: null,
|
|
29
|
+
loadedData: undefined,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function toError(error: unknown): Error {
|
|
33
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Execute all variant loaders and merge their payloads.
|
|
38
|
+
* `loadedData` is applied last so host-level overrides win.
|
|
39
|
+
*/
|
|
40
|
+
export async function executeVariantLoaders(
|
|
41
|
+
loaders: VariantLoader[] | undefined,
|
|
42
|
+
loadedData?: Record<string, unknown>,
|
|
43
|
+
): Promise<Record<string, unknown> | undefined> {
|
|
44
|
+
const hasLoaders = !!loaders && loaders.length > 0;
|
|
45
|
+
if (!hasLoaders) {
|
|
46
|
+
return loadedData;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const results = await Promise.all(loaders.map((loader) => loader()));
|
|
50
|
+
const mergedFromLoaders = results.reduce<Record<string, unknown>>(
|
|
51
|
+
(acc, result) => ({ ...acc, ...result }),
|
|
52
|
+
{},
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return loadedData ? { ...mergedFromLoaders, ...loadedData } : mergedFromLoaders;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Resolve a full runtime state (loader execution + render) in one async call.
|
|
60
|
+
* This is useful for testing and for hook/component orchestration.
|
|
61
|
+
*/
|
|
62
|
+
export async function resolvePreviewRuntimeState(
|
|
63
|
+
options: PreviewRuntimeOptions,
|
|
64
|
+
): Promise<PreviewRuntimeState> {
|
|
65
|
+
const { variant, loadedData } = options;
|
|
66
|
+
if (!variant) {
|
|
67
|
+
return EMPTY_STATE;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const mergedLoadedData = await executeVariantLoaders(variant.loaders, loadedData);
|
|
72
|
+
const content = variant.render({ loadedData: mergedLoadedData });
|
|
73
|
+
return {
|
|
74
|
+
content,
|
|
75
|
+
isLoading: false,
|
|
76
|
+
error: null,
|
|
77
|
+
loadedData: mergedLoadedData,
|
|
78
|
+
};
|
|
79
|
+
} catch (error) {
|
|
80
|
+
return {
|
|
81
|
+
content: null,
|
|
82
|
+
isLoading: false,
|
|
83
|
+
error: toError(error),
|
|
84
|
+
loadedData: undefined,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Hook for rendering a preview variant with loader support.
|
|
91
|
+
*/
|
|
92
|
+
export function usePreviewVariantRuntime(
|
|
93
|
+
options: PreviewRuntimeOptions,
|
|
94
|
+
): PreviewRuntimeState {
|
|
95
|
+
const { variant, loadedData } = options;
|
|
96
|
+
const [state, setState] = useState<PreviewRuntimeState>(EMPTY_STATE);
|
|
97
|
+
|
|
98
|
+
useEffect(() => {
|
|
99
|
+
let cancelled = false;
|
|
100
|
+
|
|
101
|
+
if (!variant) {
|
|
102
|
+
setState(EMPTY_STATE);
|
|
103
|
+
return () => {
|
|
104
|
+
cancelled = true;
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const hasLoaders = !!variant.loaders && variant.loaders.length > 0;
|
|
109
|
+
setState({
|
|
110
|
+
content: null,
|
|
111
|
+
isLoading: hasLoaders,
|
|
112
|
+
error: null,
|
|
113
|
+
loadedData: undefined,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
resolvePreviewRuntimeState({ variant, loadedData }).then((nextState) => {
|
|
117
|
+
if (!cancelled) {
|
|
118
|
+
setState(nextState);
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return () => {
|
|
123
|
+
cancelled = true;
|
|
124
|
+
};
|
|
125
|
+
}, [variant, loadedData]);
|
|
126
|
+
|
|
127
|
+
return state;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
interface PreviewVariantRuntimeProps extends PreviewRuntimeOptions {
|
|
131
|
+
children: (state: PreviewRuntimeState) => ReactNode;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Render-prop component wrapper around `usePreviewVariantRuntime`.
|
|
136
|
+
*/
|
|
137
|
+
export function PreviewVariantRuntime({
|
|
138
|
+
variant,
|
|
139
|
+
loadedData,
|
|
140
|
+
children,
|
|
141
|
+
}: PreviewVariantRuntimeProps) {
|
|
142
|
+
const state = usePreviewVariantRuntime({ variant, loadedData });
|
|
143
|
+
return <>{children(state)}</>;
|
|
144
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
|
|
3
|
+
import { isEnforceableHtmlEquivalent } from "./raw-html-canonical.js";
|
|
4
|
+
|
|
5
|
+
describe("isEnforceableHtmlEquivalent — library-agnostic enforcement gate", () => {
|
|
6
|
+
it("always enforces low-frequency semantic tags for any canonical name", () => {
|
|
7
|
+
// Agnostic already: a custom-named component on a semantic tag.
|
|
8
|
+
expect(isEnforceableHtmlEquivalent({ tag: "textarea", canonical: "AcmeTextArea" })).toBe(true);
|
|
9
|
+
expect(isEnforceableHtmlEquivalent({ tag: "dialog", canonical: "Modal" })).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("enforces an EXPLICITLY-declared button mapping against ANY component name (agnostic)", () => {
|
|
13
|
+
// The fix: @acme/ui's differently-named canonical button now fires on a raw
|
|
14
|
+
// <button>, not only the curated built-in "Button".
|
|
15
|
+
expect(
|
|
16
|
+
isEnforceableHtmlEquivalent({ tag: "button", canonical: "AcmeButton", explicit: true })
|
|
17
|
+
).toBe(true);
|
|
18
|
+
expect(isEnforceableHtmlEquivalent({ tag: "a", canonical: "AcmeLink", explicit: true })).toBe(
|
|
19
|
+
true
|
|
20
|
+
);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("keeps the built-in agreement check for INFERRED button mappings (flood guard)", () => {
|
|
24
|
+
// No `explicit` provenance → an inferred htmlEquivalent must still agree with
|
|
25
|
+
// the curated canonical. This is the <button> → Chip flood class (#229).
|
|
26
|
+
expect(isEnforceableHtmlEquivalent({ tag: "button", canonical: "Chip" })).toBe(false);
|
|
27
|
+
// Agreement (name === curated) still fires without an explicit flag.
|
|
28
|
+
expect(isEnforceableHtmlEquivalent({ tag: "button", canonical: "Button" })).toBe(true);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("NEVER enforces generic containers, even when explicitly declared", () => {
|
|
32
|
+
// The load-bearing flood guard: an explicit div/span declaration must not
|
|
33
|
+
// turn every container into a bespoke re-implementation.
|
|
34
|
+
expect(isEnforceableHtmlEquivalent({ tag: "div", canonical: "MyCard", explicit: true })).toBe(
|
|
35
|
+
false
|
|
36
|
+
);
|
|
37
|
+
expect(isEnforceableHtmlEquivalent({ tag: "span", canonical: "MyBadge", explicit: true })).toBe(
|
|
38
|
+
false
|
|
39
|
+
);
|
|
40
|
+
});
|
|
41
|
+
});
|