@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,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend Contract — the versioned, content-addressed artifact that is both
|
|
3
|
+
* served to the agent at generation time and enforced in CI. This barrel
|
|
4
|
+
* exposes the cross-runtime hashing keystone (the FCID), the unified preimage
|
|
5
|
+
* projection that defines what the contract IS, the shared pin formatter, and
|
|
6
|
+
* the committed reference catalog (`catalog-fixture-a`).
|
|
7
|
+
*/
|
|
8
|
+
export { canonicalPreimage, contractHash, sha256Hex } from "./hash.js";
|
|
9
|
+
export {
|
|
10
|
+
CONTRACT_DOMAINS,
|
|
11
|
+
CONTRACT_PREIMAGE_SCHEMA,
|
|
12
|
+
contractComponentsFromFragments,
|
|
13
|
+
contractPolicyFromGovernanceConfig,
|
|
14
|
+
diffContractDomains,
|
|
15
|
+
projectContractPreimage,
|
|
16
|
+
} from "./preimage.js";
|
|
17
|
+
export type {
|
|
18
|
+
CanonicalMappingStatus,
|
|
19
|
+
ContractCanonicalMappingInput,
|
|
20
|
+
ContractCatalogInput,
|
|
21
|
+
ContractComponentInput,
|
|
22
|
+
ContractDomain,
|
|
23
|
+
ContractPolicyInput,
|
|
24
|
+
ContractPreimage,
|
|
25
|
+
ContractPropMappingInput,
|
|
26
|
+
ContractTokenInput,
|
|
27
|
+
ContractWaiverInput,
|
|
28
|
+
} from "./preimage.js";
|
|
29
|
+
export {
|
|
30
|
+
AGENT_CONTEXT_RELATIVE_PATH,
|
|
31
|
+
formatStampHeader,
|
|
32
|
+
formatStampPin,
|
|
33
|
+
formatStampVersionHash,
|
|
34
|
+
parseContractStamp,
|
|
35
|
+
serializeContractStamp,
|
|
36
|
+
} from "./stamp.js";
|
|
37
|
+
export type { ContractStampPin, ContractStampRecord } from "./stamp.js";
|
|
38
|
+
export {
|
|
39
|
+
catalogFixtureA,
|
|
40
|
+
catalogFixtureAContractPayload,
|
|
41
|
+
catalogFixtureATokensDtcg,
|
|
42
|
+
CATALOG_FIXTURE_A_FCID,
|
|
43
|
+
} from "./fixture.js";
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Contract-identity ACCEPTANCE §1–§4 (apps/cloud/docs/contract-identity).
|
|
3
|
+
* `catalog-fixture-a` + golden fcid live in `fixture.ts`; the Cloud-side parity
|
|
4
|
+
* half of §1 imports the same constants.
|
|
5
|
+
*/
|
|
6
|
+
import { describe, expect, it } from "vitest";
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
CATALOG_FIXTURE_A_FCID,
|
|
10
|
+
catalogFixtureA,
|
|
11
|
+
catalogFixtureAContractPayload,
|
|
12
|
+
} from "./fixture.js";
|
|
13
|
+
import { contractHash } from "./hash.js";
|
|
14
|
+
import {
|
|
15
|
+
type ContractCatalogInput,
|
|
16
|
+
type ContractComponentInput,
|
|
17
|
+
type ContractTokenInput,
|
|
18
|
+
contractComponentsFromFragments,
|
|
19
|
+
diffContractDomains,
|
|
20
|
+
projectContractPreimage,
|
|
21
|
+
} from "./preimage.js";
|
|
22
|
+
import { formatStampPin } from "./stamp.js";
|
|
23
|
+
|
|
24
|
+
function fcidOf(catalog: ContractCatalogInput): string {
|
|
25
|
+
return contractHash(projectContractPreimage(catalog));
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
describe("§1 byte parity — one fixture, one hash, every runtime", () => {
|
|
29
|
+
it("hashes catalog-fixture-a to the committed golden fcid", () => {
|
|
30
|
+
expect(fcidOf(catalogFixtureA)).toBe(CATALOG_FIXTURE_A_FCID);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
it("is independent of adapter iteration order", () => {
|
|
34
|
+
const reordered: ContractCatalogInput = {
|
|
35
|
+
...catalogFixtureA,
|
|
36
|
+
components: [...(catalogFixtureA.components ?? [])].reverse().map((component) => ({
|
|
37
|
+
...component,
|
|
38
|
+
props: component.props ? [...component.props].reverse() : undefined,
|
|
39
|
+
})),
|
|
40
|
+
tokens: [...(catalogFixtureA.tokens ?? [])].reverse(),
|
|
41
|
+
canonicalMappings: [...(catalogFixtureA.canonicalMappings ?? [])].reverse(),
|
|
42
|
+
};
|
|
43
|
+
expect(fcidOf(reordered)).toBe(CATALOG_FIXTURE_A_FCID);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("hashes the empty catalog deterministically too", () => {
|
|
47
|
+
expect(fcidOf({})).toMatch(/^[0-9a-f]{64}$/);
|
|
48
|
+
expect(fcidOf({})).toBe(fcidOf({ components: [], tokens: [], policy: null }));
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("the payload companion assembles to the fixture's components domain", () => {
|
|
52
|
+
// Guards the Cloud parity materials: editing `catalogFixtureA` without its
|
|
53
|
+
// payload-shaped companion (or vice versa) fails here, in core, before the
|
|
54
|
+
// Convex test ever runs.
|
|
55
|
+
expect(
|
|
56
|
+
fcidOf({
|
|
57
|
+
...catalogFixtureA,
|
|
58
|
+
components: contractComponentsFromFragments(catalogFixtureAContractPayload.fragments),
|
|
59
|
+
})
|
|
60
|
+
).toBe(CATALOG_FIXTURE_A_FCID);
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("§2 enforced-only — prose doesn't move the hash, enforcement does", () => {
|
|
65
|
+
it("(a) display-data edits leave the identity unchanged", () => {
|
|
66
|
+
// Adapters may pass wider objects (enrichment descriptions, docs URLs…);
|
|
67
|
+
// the projection's pick-list is the enforced-only boundary.
|
|
68
|
+
const withProseEdits: ContractCatalogInput = {
|
|
69
|
+
...catalogFixtureA,
|
|
70
|
+
components: (catalogFixtureA.components ?? []).map(
|
|
71
|
+
(component) =>
|
|
72
|
+
({
|
|
73
|
+
...component,
|
|
74
|
+
description: "A lovingly rewritten description",
|
|
75
|
+
docsUrl: "https://usefragments.com/docs/button",
|
|
76
|
+
}) as ContractComponentInput
|
|
77
|
+
),
|
|
78
|
+
tokens: (catalogFixtureA.tokens ?? []).map(
|
|
79
|
+
(token) => ({ ...token, description: "now with prose" }) as ContractTokenInput
|
|
80
|
+
),
|
|
81
|
+
};
|
|
82
|
+
expect(fcidOf(withProseEdits)).toBe(CATALOG_FIXTURE_A_FCID);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("(b) confirming a mapping changes the identity — and only the canonicalMap domain", () => {
|
|
86
|
+
const withConfirmedMapping: ContractCatalogInput = {
|
|
87
|
+
...catalogFixtureA,
|
|
88
|
+
canonicalMappings: (catalogFixtureA.canonicalMappings ?? []).map((mapping) =>
|
|
89
|
+
mapping.status === "proposed" ? { ...mapping, status: "confirmed" as const } : mapping
|
|
90
|
+
),
|
|
91
|
+
};
|
|
92
|
+
expect(fcidOf(withConfirmedMapping)).not.toBe(CATALOG_FIXTURE_A_FCID);
|
|
93
|
+
expect(
|
|
94
|
+
diffContractDomains(
|
|
95
|
+
projectContractPreimage(catalogFixtureA),
|
|
96
|
+
projectContractPreimage(withConfirmedMapping)
|
|
97
|
+
)
|
|
98
|
+
).toEqual(["canonicalMap"]);
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
describe("§3 proposed mappings are not identity", () => {
|
|
103
|
+
it("adding a new proposed mapping leaves the fcid unchanged", () => {
|
|
104
|
+
const withNewProposedMapping: ContractCatalogInput = {
|
|
105
|
+
...catalogFixtureA,
|
|
106
|
+
canonicalMappings: [
|
|
107
|
+
...(catalogFixtureA.canonicalMappings ?? []),
|
|
108
|
+
{ component: "AppButton", canonical: "Button", status: "proposed" as const },
|
|
109
|
+
],
|
|
110
|
+
};
|
|
111
|
+
expect(fcidOf(withNewProposedMapping)).toBe(CATALOG_FIXTURE_A_FCID);
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe("§4 domain diff names what moved", () => {
|
|
116
|
+
it("confirming a mapping + adding a composition rule changes canonicalMap and policy", () => {
|
|
117
|
+
const preimageBase = projectContractPreimage(catalogFixtureA);
|
|
118
|
+
const vNext: ContractCatalogInput = {
|
|
119
|
+
...catalogFixtureA,
|
|
120
|
+
canonicalMappings: (catalogFixtureA.canonicalMappings ?? []).map((mapping) =>
|
|
121
|
+
mapping.status === "proposed" ? { ...mapping, status: "confirmed" as const } : mapping
|
|
122
|
+
),
|
|
123
|
+
policy: {
|
|
124
|
+
...catalogFixtureA.policy,
|
|
125
|
+
compositionPatterns: [
|
|
126
|
+
...(catalogFixtureA.policy?.compositionPatterns ?? []),
|
|
127
|
+
{ id: "dialog-structure", region: "Dialog", require: ["Dialog.Title"] },
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
};
|
|
131
|
+
const preimageNext = projectContractPreimage(vNext);
|
|
132
|
+
expect(diffContractDomains(preimageBase, preimageNext).sort()).toEqual([
|
|
133
|
+
"canonicalMap",
|
|
134
|
+
"policy",
|
|
135
|
+
]);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("identical preimages diff to nothing", () => {
|
|
139
|
+
const preimage = projectContractPreimage(catalogFixtureA);
|
|
140
|
+
expect(diffContractDomains(preimage, projectContractPreimage(catalogFixtureA))).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("fcid-preimage:v1 package identity compatibility", () => {
|
|
145
|
+
it("keeps old/new owned canonical-map imports byte-identical", () => {
|
|
146
|
+
const historical: ContractCatalogInput = {
|
|
147
|
+
canonicalMappings: [
|
|
148
|
+
{
|
|
149
|
+
component: "button",
|
|
150
|
+
canonical: "Button",
|
|
151
|
+
status: "confirmed",
|
|
152
|
+
importPath: "@fragments-sdk/ui",
|
|
153
|
+
},
|
|
154
|
+
],
|
|
155
|
+
};
|
|
156
|
+
const current: ContractCatalogInput = {
|
|
157
|
+
canonicalMappings: [
|
|
158
|
+
{
|
|
159
|
+
component: "button",
|
|
160
|
+
canonical: "Button",
|
|
161
|
+
status: "confirmed",
|
|
162
|
+
importPath: "@usefragments/ui",
|
|
163
|
+
},
|
|
164
|
+
],
|
|
165
|
+
};
|
|
166
|
+
expect(projectContractPreimage(current)).toEqual(projectContractPreimage(historical));
|
|
167
|
+
expect(fcidOf(current)).toBe(fcidOf(historical));
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("projects only typed policy identity fields and leaves provenance/repository bytes alone", () => {
|
|
171
|
+
const historical: ContractCatalogInput = {
|
|
172
|
+
policy: {
|
|
173
|
+
rules: {
|
|
174
|
+
canonicalSources: [{ kind: "npm", specifier: "@fragments-sdk/ui" }],
|
|
175
|
+
importRule: {
|
|
176
|
+
kind: "jsx.importPath.prefer",
|
|
177
|
+
from: "@fragments-sdk/ui/legacy",
|
|
178
|
+
to: "@fragments-sdk/ui",
|
|
179
|
+
},
|
|
180
|
+
match: { componentId: "@fragments-sdk/ui#Button" },
|
|
181
|
+
implementationProvenance: "@repo/cloud-ui",
|
|
182
|
+
repository: "fragments-sdk/fragments",
|
|
183
|
+
generated_by: "@fragments-sdk/ui",
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
const current: ContractCatalogInput = {
|
|
188
|
+
policy: {
|
|
189
|
+
rules: {
|
|
190
|
+
canonicalSources: [{ kind: "npm", specifier: "@usefragments/ui" }],
|
|
191
|
+
importRule: {
|
|
192
|
+
kind: "jsx.importPath.prefer",
|
|
193
|
+
from: "@usefragments/ui/legacy",
|
|
194
|
+
to: "@usefragments/ui",
|
|
195
|
+
},
|
|
196
|
+
match: { componentId: "@usefragments/ui#Button" },
|
|
197
|
+
implementationProvenance: "@repo/cloud-ui",
|
|
198
|
+
repository: "fragments-sdk/fragments",
|
|
199
|
+
// A generic marker is not an npm-identity field and must not be
|
|
200
|
+
// normalized just because its value resembles a package.
|
|
201
|
+
generated_by: "@fragments-sdk/ui",
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
};
|
|
205
|
+
expect(projectContractPreimage(current)).toEqual(projectContractPreimage(historical));
|
|
206
|
+
|
|
207
|
+
const changedMarker: ContractCatalogInput = {
|
|
208
|
+
policy: {
|
|
209
|
+
rules: {
|
|
210
|
+
...(current.policy?.rules ?? {}),
|
|
211
|
+
generated_by: "@usefragments/ui",
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
expect(projectContractPreimage(changedMarker).domains.policy).not.toBe(
|
|
216
|
+
projectContractPreimage(current).domains.policy
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
it("does not normalize third-party package identities", () => {
|
|
221
|
+
const acme = projectContractPreimage({
|
|
222
|
+
canonicalMappings: [
|
|
223
|
+
{
|
|
224
|
+
component: "button",
|
|
225
|
+
canonical: "Button",
|
|
226
|
+
status: "confirmed",
|
|
227
|
+
importPath: "@acme/ui",
|
|
228
|
+
},
|
|
229
|
+
],
|
|
230
|
+
});
|
|
231
|
+
const other = projectContractPreimage({
|
|
232
|
+
canonicalMappings: [
|
|
233
|
+
{
|
|
234
|
+
component: "button",
|
|
235
|
+
canonical: "Button",
|
|
236
|
+
status: "confirmed",
|
|
237
|
+
importPath: "@other/ui",
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
});
|
|
241
|
+
expect(acme.domains.canonicalMap).not.toBe(other.domains.canonicalMap);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
describe("formatStampPin", () => {
|
|
246
|
+
it("renders the display convention: version + abbreviated fcid", () => {
|
|
247
|
+
expect(
|
|
248
|
+
formatStampPin({
|
|
249
|
+
contractVersion: 12,
|
|
250
|
+
fcid: "a3f9e21b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b785",
|
|
251
|
+
})
|
|
252
|
+
).toBe("contract v12 · a3f9e21");
|
|
253
|
+
});
|
|
254
|
+
});
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Frontend Contract preimage — the projection that defines what the contract IS.
|
|
3
|
+
*
|
|
4
|
+
* The FCID used to be a hash of the raw `code-contracts` artifact payload. That
|
|
5
|
+
* conflated identity with display: a copy-edit to a component description moved
|
|
6
|
+
* the hash, and tokens / canonical mappings / policy — the things gates actually
|
|
7
|
+
* enforce — were not part of identity at all. This module widens the preimage to
|
|
8
|
+
* the full enforcement surface as ONE shared projection, imported by both Cloud
|
|
9
|
+
* (mint) and the CLI (reportedHash). Neither side may reimplement it: parity is
|
|
10
|
+
* a property of the shared import, not a test you can fail.
|
|
11
|
+
*
|
|
12
|
+
* Enforced-only is implemented structurally: {@link projectContractPreimage}
|
|
13
|
+
* copies an explicit pick-list of fields per domain. Anything an adapter passes
|
|
14
|
+
* beyond that list (descriptions, docs URLs, timestamps, heuristic signals)
|
|
15
|
+
* never reaches the hash. Proposed canonical mappings are excluded for the same
|
|
16
|
+
* reason — they cannot block, so they are not identity; confirmation is the
|
|
17
|
+
* moment a mapping enters the contract.
|
|
18
|
+
*
|
|
19
|
+
* The preimage itself is the record of four domain sub-hashes, so
|
|
20
|
+
* `contractHash(preimage)` is the fcid and a domain diff is a four-string
|
|
21
|
+
* compare — no deep structural diffing. Determinism (key sorting, undefined
|
|
22
|
+
* dropping, number normalization) is inherited from `canonicalPreimage`; this
|
|
23
|
+
* module adds element-level sorting for the arrays it owns and nothing else.
|
|
24
|
+
*
|
|
25
|
+
* Runtime-portable by construction: no Node APIs, no Convex imports, no React.
|
|
26
|
+
*/
|
|
27
|
+
import type { GovernanceConfig } from "../governance.js";
|
|
28
|
+
import { projectV1OwnedComponentId, projectV1OwnedImportIdentity } from "../package-identity.js";
|
|
29
|
+
import { canonicalPreimage, contractHash } from "./hash.js";
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// Domains
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The fixed domain enum. Adding a domain is a spine change (update
|
|
37
|
+
* `01-architecture.md` first); never rename — deprecate and alias.
|
|
38
|
+
*/
|
|
39
|
+
export const CONTRACT_DOMAINS = ["components", "tokens", "canonicalMap", "policy"] as const;
|
|
40
|
+
|
|
41
|
+
export type ContractDomain = (typeof CONTRACT_DOMAINS)[number];
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------
|
|
44
|
+
// Catalog input — the neutral shape adapters assemble
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* One component's enforcement identity. Props are the *name* membership surface
|
|
49
|
+
* (what unknown-prop checks today); prop types/defaults are not enforced by the
|
|
50
|
+
* rules engine, so they are not identity — enforced-only cuts both ways.
|
|
51
|
+
*/
|
|
52
|
+
export interface ContractComponentInput {
|
|
53
|
+
name: string;
|
|
54
|
+
/** Parent component name when this is a subcomponent (compound identity). */
|
|
55
|
+
parentName?: string;
|
|
56
|
+
/** Prop names the component accepts. */
|
|
57
|
+
props?: readonly string[];
|
|
58
|
+
/**
|
|
59
|
+
* The per-component enforcement contract as authored (bans, a11y rules,
|
|
60
|
+
* required compound children). Passed through whole — it is enforcement data
|
|
61
|
+
* by definition.
|
|
62
|
+
*/
|
|
63
|
+
contract?: unknown;
|
|
64
|
+
/** Required compound-children names, when the structure is declared. */
|
|
65
|
+
compoundChildren?: readonly string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ContractTokenInput {
|
|
69
|
+
name: string;
|
|
70
|
+
value: string | number;
|
|
71
|
+
type?: string;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export type CanonicalMappingStatus = "confirmed" | "proposed" | "unknown";
|
|
75
|
+
|
|
76
|
+
export interface ContractPropMappingInput {
|
|
77
|
+
rawProp: string;
|
|
78
|
+
canonicalProp: string;
|
|
79
|
+
valueMap?: Readonly<Record<string, string>>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface ContractCanonicalMappingInput {
|
|
83
|
+
/** The raw/source component the mapping covers. */
|
|
84
|
+
component: string;
|
|
85
|
+
/** The canonical component it maps to. */
|
|
86
|
+
canonical: string;
|
|
87
|
+
status: CanonicalMappingStatus;
|
|
88
|
+
/** Local/offline canonical import target, when the mapping is config-authored. */
|
|
89
|
+
importPath?: string;
|
|
90
|
+
/** Explicit raw element or semantic resolutions covered by a local declaration. */
|
|
91
|
+
resolves?: readonly {
|
|
92
|
+
tag: string;
|
|
93
|
+
role?: string;
|
|
94
|
+
inputType?: string;
|
|
95
|
+
}[];
|
|
96
|
+
propMapping?: readonly ContractPropMappingInput[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* An authored waiver record. Hashed as data — including `expiresOn` as a plain
|
|
101
|
+
* date string. Expiry *evaluation* happens at enforcement time and must never
|
|
102
|
+
* change the fcid (no time-dependent hashing).
|
|
103
|
+
*/
|
|
104
|
+
export interface ContractWaiverInput {
|
|
105
|
+
id: string;
|
|
106
|
+
/** The diagnostic code or rule the waiver suppresses. */
|
|
107
|
+
target: string;
|
|
108
|
+
reason: string;
|
|
109
|
+
/** YYYY-MM-DD, treated as opaque data. */
|
|
110
|
+
expiresOn?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Active policy: everything that changes what a gate would allow or deny.
|
|
115
|
+
* Waivers live here (spine open question 1, resolved): a waiver changes what
|
|
116
|
+
* blocks, so it is enforcement-relevant.
|
|
117
|
+
*/
|
|
118
|
+
export interface ContractPolicyInput {
|
|
119
|
+
rules?: Readonly<Record<string, unknown>>;
|
|
120
|
+
codes?: Readonly<Record<string, unknown>>;
|
|
121
|
+
compositionPatterns?: readonly unknown[];
|
|
122
|
+
waivers?: readonly ContractWaiverInput[];
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* The neutral catalog shape both adapters (Cloud mint, CLI scan) assemble.
|
|
127
|
+
* Adapters gather data; the projection owns all normalization — element order,
|
|
128
|
+
* optional fields, and display-data stripping never reach the hash.
|
|
129
|
+
*/
|
|
130
|
+
export interface ContractCatalogInput {
|
|
131
|
+
components?: readonly ContractComponentInput[];
|
|
132
|
+
tokens?: readonly ContractTokenInput[];
|
|
133
|
+
canonicalMappings?: readonly ContractCanonicalMappingInput[];
|
|
134
|
+
policy?: ContractPolicyInput | null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---------------------------------------------------------------------------
|
|
138
|
+
// Preimage
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
|
|
141
|
+
export const CONTRACT_PREIMAGE_SCHEMA = "fcid-preimage:v1" as const;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The hashed identity record: four domain sub-hashes plus the projection schema
|
|
145
|
+
* version (so an intentional projection change mints a new fcid). The fcid is
|
|
146
|
+
* `contractHash(preimage)` — hash of the domain hashes, per the spine pipeline.
|
|
147
|
+
*/
|
|
148
|
+
export interface ContractPreimage {
|
|
149
|
+
schema: typeof CONTRACT_PREIMAGE_SCHEMA;
|
|
150
|
+
domains: Record<ContractDomain, string>;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Sort projected elements by their canonical serialization: total, deterministic,
|
|
155
|
+
* and independent of adapter iteration order, without per-type comparators.
|
|
156
|
+
*/
|
|
157
|
+
function sortCanonical<T>(items: readonly T[]): T[] {
|
|
158
|
+
return items
|
|
159
|
+
.map((item) => ({ item, key: canonicalPreimage(item) }))
|
|
160
|
+
.sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
|
|
161
|
+
.map(({ item }) => item);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function projectComponents(components: readonly ContractComponentInput[] | undefined): unknown {
|
|
165
|
+
return sortCanonical(
|
|
166
|
+
(components ?? []).map((component) => ({
|
|
167
|
+
name: component.name,
|
|
168
|
+
parentName: component.parentName,
|
|
169
|
+
props: component.props ? [...component.props].sort() : undefined,
|
|
170
|
+
contract: component.contract,
|
|
171
|
+
compoundChildren: component.compoundChildren
|
|
172
|
+
? [...component.compoundChildren].sort()
|
|
173
|
+
: undefined,
|
|
174
|
+
}))
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function projectTokens(tokens: readonly ContractTokenInput[] | undefined): unknown {
|
|
179
|
+
return sortCanonical(
|
|
180
|
+
(tokens ?? []).map((token) => ({
|
|
181
|
+
name: token.name,
|
|
182
|
+
value: token.value,
|
|
183
|
+
type: token.type,
|
|
184
|
+
}))
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function projectCanonicalMap(
|
|
189
|
+
mappings: readonly ContractCanonicalMappingInput[] | undefined
|
|
190
|
+
): unknown {
|
|
191
|
+
return sortCanonical(
|
|
192
|
+
(mappings ?? [])
|
|
193
|
+
.filter((mapping) => mapping.status === "confirmed")
|
|
194
|
+
.map((mapping) => ({
|
|
195
|
+
component: mapping.component,
|
|
196
|
+
canonical: mapping.canonical,
|
|
197
|
+
importPath: mapping.importPath
|
|
198
|
+
? projectV1OwnedImportIdentity(mapping.importPath)
|
|
199
|
+
: undefined,
|
|
200
|
+
resolves: mapping.resolves ? sortCanonical(mapping.resolves) : undefined,
|
|
201
|
+
propMapping: mapping.propMapping
|
|
202
|
+
? sortCanonical(
|
|
203
|
+
mapping.propMapping.map((entry) => ({
|
|
204
|
+
rawProp: entry.rawProp,
|
|
205
|
+
canonicalProp: entry.canonicalProp,
|
|
206
|
+
valueMap: entry.valueMap,
|
|
207
|
+
}))
|
|
208
|
+
)
|
|
209
|
+
: undefined,
|
|
210
|
+
}))
|
|
211
|
+
);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Project only structured npm-identity fields inside policy material. This is
|
|
216
|
+
* deliberately key- and record-aware: arbitrary strings, repository names,
|
|
217
|
+
* provenance, product markers, and third-party package values retain their
|
|
218
|
+
* original bytes.
|
|
219
|
+
*/
|
|
220
|
+
function projectV1PolicyMaterial(value: unknown, key?: string): unknown {
|
|
221
|
+
if (typeof value === "string") {
|
|
222
|
+
if (key === "specifier" || key === "importPath") {
|
|
223
|
+
return projectV1OwnedImportIdentity(value);
|
|
224
|
+
}
|
|
225
|
+
if (key === "componentId") {
|
|
226
|
+
return projectV1OwnedComponentId(value);
|
|
227
|
+
}
|
|
228
|
+
return value;
|
|
229
|
+
}
|
|
230
|
+
if (Array.isArray(value)) {
|
|
231
|
+
return value.map((entry) => projectV1PolicyMaterial(entry));
|
|
232
|
+
}
|
|
233
|
+
if (!value || typeof value !== "object") return value;
|
|
234
|
+
|
|
235
|
+
const record = value as Readonly<Record<string, unknown>>;
|
|
236
|
+
const isImportPathRule = record.kind === "jsx.importPath.prefer";
|
|
237
|
+
return Object.fromEntries(
|
|
238
|
+
Object.entries(record).map(([field, fieldValue]) => {
|
|
239
|
+
if (
|
|
240
|
+
isImportPathRule &&
|
|
241
|
+
(field === "from" || field === "to") &&
|
|
242
|
+
typeof fieldValue === "string"
|
|
243
|
+
) {
|
|
244
|
+
return [field, projectV1OwnedImportIdentity(fieldValue)];
|
|
245
|
+
}
|
|
246
|
+
return [field, projectV1PolicyMaterial(fieldValue, field)];
|
|
247
|
+
})
|
|
248
|
+
);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function projectPolicy(policy: ContractPolicyInput | null | undefined): unknown {
|
|
252
|
+
if (!policy) return null;
|
|
253
|
+
return {
|
|
254
|
+
rules: projectV1PolicyMaterial(policy.rules),
|
|
255
|
+
codes: policy.codes,
|
|
256
|
+
compositionPatterns: policy.compositionPatterns
|
|
257
|
+
? sortCanonical(policy.compositionPatterns)
|
|
258
|
+
: undefined,
|
|
259
|
+
waivers: policy.waivers
|
|
260
|
+
? sortCanonical(
|
|
261
|
+
policy.waivers.map((waiver) => ({
|
|
262
|
+
id: waiver.id,
|
|
263
|
+
target: waiver.target,
|
|
264
|
+
reason: waiver.reason,
|
|
265
|
+
expiresOn: waiver.expiresOn,
|
|
266
|
+
}))
|
|
267
|
+
)
|
|
268
|
+
: undefined,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Project a catalog onto the contract preimage: four enforced-only domain
|
|
274
|
+
* bodies, each hashed independently. `contractHash(projectContractPreimage(x))`
|
|
275
|
+
* is the fcid.
|
|
276
|
+
*/
|
|
277
|
+
export function projectContractPreimage(catalog: ContractCatalogInput): ContractPreimage {
|
|
278
|
+
return {
|
|
279
|
+
schema: CONTRACT_PREIMAGE_SCHEMA,
|
|
280
|
+
domains: {
|
|
281
|
+
components: contractHash(projectComponents(catalog.components)),
|
|
282
|
+
tokens: contractHash(projectTokens(catalog.tokens)),
|
|
283
|
+
canonicalMap: contractHash(projectCanonicalMap(catalog.canonicalMappings)),
|
|
284
|
+
policy: contractHash(projectPolicy(catalog.policy)),
|
|
285
|
+
},
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Name the domains whose identity differs between two preimages — a four-string
|
|
291
|
+
* compare, no structural diffing. Returned in fixed {@link CONTRACT_DOMAINS}
|
|
292
|
+
* order. Staleness checks compare these, never whole fcids.
|
|
293
|
+
*/
|
|
294
|
+
export function diffContractDomains(a: ContractPreimage, b: ContractPreimage): ContractDomain[] {
|
|
295
|
+
return CONTRACT_DOMAINS.filter((domain) => a.domains[domain] !== b.domains[domain]);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Map defineFragment-shaped records — the `code-contracts` payload's
|
|
300
|
+
* `fragments` array (Cloud mint) or the CLI's loaded fragment definitions —
|
|
301
|
+
* onto their enforcement identity. The one shared mapping is what keeps the
|
|
302
|
+
* two adapters from drifting into a second "catalog → preimage"
|
|
303
|
+
* implementation: both sides gather fragments however they like, then this
|
|
304
|
+
* picks the identity fields. Records without a string `meta.name` are skipped
|
|
305
|
+
* (no identity to hash).
|
|
306
|
+
*/
|
|
307
|
+
/**
|
|
308
|
+
* The enforced-only slice of a resolved CLI governance config — the fields
|
|
309
|
+
* that change what a scan allows or denies. CI gate options, agent repair
|
|
310
|
+
* order, and runner config change how/when gates run, not what they enforce,
|
|
311
|
+
* so they are not identity. This pick-list lives here, beside the projection,
|
|
312
|
+
* so adapters cannot drift into private enforced-only boundaries: a new
|
|
313
|
+
* enforcement field on {@link GovernanceConfig} is added to the contract in
|
|
314
|
+
* exactly one place.
|
|
315
|
+
*/
|
|
316
|
+
export function contractPolicyFromGovernanceConfig(
|
|
317
|
+
config: GovernanceConfig | null | undefined
|
|
318
|
+
): ContractPolicyInput | null {
|
|
319
|
+
if (!config) return null;
|
|
320
|
+
return {
|
|
321
|
+
rules: {
|
|
322
|
+
severity: config.severity,
|
|
323
|
+
rules: config.rules,
|
|
324
|
+
scales: config.scales,
|
|
325
|
+
styles: config.styles,
|
|
326
|
+
jsx: config.jsx,
|
|
327
|
+
tailwind: config.tailwind,
|
|
328
|
+
components: config.components,
|
|
329
|
+
overrides: config.overrides,
|
|
330
|
+
},
|
|
331
|
+
codes: config.codes as Readonly<Record<string, unknown>> | undefined,
|
|
332
|
+
compositionPatterns: config.compositionPatterns as readonly unknown[] | undefined,
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export function contractComponentsFromFragments(
|
|
337
|
+
fragments: readonly unknown[]
|
|
338
|
+
): ContractComponentInput[] {
|
|
339
|
+
const components: ContractComponentInput[] = [];
|
|
340
|
+
for (const candidate of fragments) {
|
|
341
|
+
const fragment = candidate as {
|
|
342
|
+
meta?: { name?: unknown; parentComponentName?: unknown };
|
|
343
|
+
props?: Record<string, unknown>;
|
|
344
|
+
contract?: unknown;
|
|
345
|
+
structure?: { compoundChildren?: unknown };
|
|
346
|
+
} | null;
|
|
347
|
+
const name = fragment?.meta?.name;
|
|
348
|
+
if (typeof name !== "string" || !name) continue;
|
|
349
|
+
const parentName = fragment?.meta?.parentComponentName;
|
|
350
|
+
const rawChildren = fragment?.structure?.compoundChildren;
|
|
351
|
+
const compoundChildren = Array.isArray(rawChildren)
|
|
352
|
+
? rawChildren
|
|
353
|
+
.map((child) => (child as { name?: unknown } | null)?.name)
|
|
354
|
+
.filter((childName): childName is string => typeof childName === "string")
|
|
355
|
+
: undefined;
|
|
356
|
+
components.push({
|
|
357
|
+
name,
|
|
358
|
+
parentName: typeof parentName === "string" ? parentName : undefined,
|
|
359
|
+
props: Object.keys(fragment?.props ?? {}),
|
|
360
|
+
contract: fragment?.contract,
|
|
361
|
+
compoundChildren,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
return components;
|
|
365
|
+
}
|