@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.
Files changed (228) hide show
  1. package/LICENSE +21 -0
  2. package/dist/chunk-243QYRUF.js +2145 -0
  3. package/dist/chunk-243QYRUF.js.map +1 -0
  4. package/dist/chunk-3LLRNCPX.js +111 -0
  5. package/dist/chunk-3LLRNCPX.js.map +1 -0
  6. package/dist/chunk-EIYNNS77.js +48 -0
  7. package/dist/chunk-EIYNNS77.js.map +1 -0
  8. package/dist/chunk-JNBFJ34I.js +326 -0
  9. package/dist/chunk-JNBFJ34I.js.map +1 -0
  10. package/dist/chunk-R3X7UBHU.js +228 -0
  11. package/dist/chunk-R3X7UBHU.js.map +1 -0
  12. package/dist/chunk-SH4KPIYH.js +393 -0
  13. package/dist/chunk-SH4KPIYH.js.map +1 -0
  14. package/dist/chunk-V4VQB57N.js +56 -0
  15. package/dist/chunk-V4VQB57N.js.map +1 -0
  16. package/dist/chunk-X34IA4LR.js +319 -0
  17. package/dist/chunk-X34IA4LR.js.map +1 -0
  18. package/dist/chunk-YF65VYRY.js +493 -0
  19. package/dist/chunk-YF65VYRY.js.map +1 -0
  20. package/dist/codes/index.d.ts +40 -0
  21. package/dist/codes/index.js +23 -0
  22. package/dist/codes/index.js.map +1 -0
  23. package/dist/compiled-types/index.d.ts +5 -0
  24. package/dist/compiled-types/index.js +1 -0
  25. package/dist/compiled-types/index.js.map +1 -0
  26. package/dist/generate/index.d.ts +47 -0
  27. package/dist/generate/index.js +11 -0
  28. package/dist/generate/index.js.map +1 -0
  29. package/dist/governance-BOa3KyiJ.d.ts +3279 -0
  30. package/dist/graph/index.d.ts +66 -0
  31. package/dist/graph/index.js +17 -0
  32. package/dist/graph/index.js.map +1 -0
  33. package/dist/index.d.ts +11388 -0
  34. package/dist/index.js +8711 -0
  35. package/dist/index.js.map +1 -0
  36. package/dist/package-identity.d.ts +42 -0
  37. package/dist/package-identity.js +23 -0
  38. package/dist/package-identity.js.map +1 -0
  39. package/dist/preview/index.d.ts +484 -0
  40. package/dist/preview/index.js +1129 -0
  41. package/dist/preview/index.js.map +1 -0
  42. package/dist/react-types.d.ts +22 -0
  43. package/dist/react-types.js +1 -0
  44. package/dist/react-types.js.map +1 -0
  45. package/dist/registry.d.ts +2367 -0
  46. package/dist/registry.js +62 -0
  47. package/dist/registry.js.map +1 -0
  48. package/dist/schemas/index.d.ts +2584 -0
  49. package/dist/schemas/index.js +55 -0
  50. package/dist/schemas/index.js.map +1 -0
  51. package/dist/severity.d.ts +18 -0
  52. package/dist/severity.js +27 -0
  53. package/dist/severity.js.map +1 -0
  54. package/dist/test-utils.d.ts +14 -0
  55. package/dist/test-utils.js +63 -0
  56. package/dist/test-utils.js.map +1 -0
  57. package/dist/topology/index.d.ts +48 -0
  58. package/dist/topology/index.js +7 -0
  59. package/dist/topology/index.js.map +1 -0
  60. package/dist/types-xJ2xyp_G.d.ts +119 -0
  61. package/package.json +120 -0
  62. package/src/__tests__/contract-parser.test.ts +614 -0
  63. package/src/__tests__/dtcg-generators.test.ts +439 -0
  64. package/src/__tests__/dtcg-parser.test.ts +884 -0
  65. package/src/__tests__/preview-runtime.test.tsx +111 -0
  66. package/src/__tests__/schema.test.ts +31 -0
  67. package/src/__tests__/severity.test.ts +34 -0
  68. package/src/__tests__/style-comparison.test.ts +662 -0
  69. package/src/__tests__/tokens-compat.test.ts +51 -0
  70. package/src/agent-format.test.ts +105 -0
  71. package/src/agent-format.ts +141 -0
  72. package/src/bundle.ts +149 -0
  73. package/src/codes/__tests__/codes.test.ts +73 -0
  74. package/src/codes/codes.ts +463 -0
  75. package/src/codes/index.ts +7 -0
  76. package/src/codes/source-finding-bridge.ts +113 -0
  77. package/src/compiled-types/index.ts +293 -0
  78. package/src/component-contract.ts +231 -0
  79. package/src/component-discovery.ts +80 -0
  80. package/src/composition.test.ts +262 -0
  81. package/src/composition.ts +318 -0
  82. package/src/config-paths.test.ts +78 -0
  83. package/src/config-paths.ts +47 -0
  84. package/src/config.ts +18 -0
  85. package/src/conform-prove.test.ts +274 -0
  86. package/src/conform-prove.ts +326 -0
  87. package/src/conform.ts +160 -0
  88. package/src/constants.ts +114 -0
  89. package/src/context.ts +2 -0
  90. package/src/contract/fixture.ts +110 -0
  91. package/src/contract/hash.test.ts +86 -0
  92. package/src/contract/hash.ts +195 -0
  93. package/src/contract/index.ts +43 -0
  94. package/src/contract/preimage.test.ts +254 -0
  95. package/src/contract/preimage.ts +365 -0
  96. package/src/contract/stamp.test.ts +100 -0
  97. package/src/contract/stamp.ts +105 -0
  98. package/src/contract-parser.ts +141 -0
  99. package/src/defineFragment.test.ts +257 -0
  100. package/src/defineFragment.ts +293 -0
  101. package/src/dtcg-generators.ts +324 -0
  102. package/src/dtcg-parser.ts +1 -0
  103. package/src/dtcg.ts +237 -0
  104. package/src/facts/__snapshots__/facts.test.ts.snap +124 -0
  105. package/src/facts/builders.ts +835 -0
  106. package/src/facts/compile.ts +422 -0
  107. package/src/facts/fact-index.ts +394 -0
  108. package/src/facts/facts.test.ts +494 -0
  109. package/src/facts/ids.ts +125 -0
  110. package/src/facts/index.ts +119 -0
  111. package/src/facts/types.ts +614 -0
  112. package/src/figma.ts +263 -0
  113. package/src/fragment-types.ts +214 -0
  114. package/src/generate/generate.test.ts +594 -0
  115. package/src/generate/index.ts +438 -0
  116. package/src/governance-integrity.test.ts +186 -0
  117. package/src/governance-integrity.ts +282 -0
  118. package/src/governance.test.ts +300 -0
  119. package/src/governance.ts +605 -0
  120. package/src/governance.type-test.ts +75 -0
  121. package/src/graph/engine.ts +446 -0
  122. package/src/graph/graph.test.ts +746 -0
  123. package/src/graph/index.ts +32 -0
  124. package/src/graph/serialization.ts +55 -0
  125. package/src/graph/types.ts +178 -0
  126. package/src/index.ts +891 -0
  127. package/src/local-canonical.ts +102 -0
  128. package/src/mcp-snapshot.ts +222 -0
  129. package/src/package-identity-match.ts +33 -0
  130. package/src/package-identity.generated.ts +233 -0
  131. package/src/package-identity.test.ts +99 -0
  132. package/src/package-identity.ts +157 -0
  133. package/src/performance-presets.ts +142 -0
  134. package/src/preview/comparison-artifact.test.ts +228 -0
  135. package/src/preview/comparison-artifact.ts +181 -0
  136. package/src/preview/comparison-context.ts +93 -0
  137. package/src/preview/comparison-evidence.ts +112 -0
  138. package/src/preview/comparison-format.ts +21 -0
  139. package/src/preview/comparison-presentation.ts +187 -0
  140. package/src/preview/comparison-references.test.ts +430 -0
  141. package/src/preview/comparison-references.ts +72 -0
  142. package/src/preview/comparison-score.ts +59 -0
  143. package/src/preview/comparison-styles.ts +197 -0
  144. package/src/preview/comparison.test.ts +559 -0
  145. package/src/preview/comparison.ts +488 -0
  146. package/src/preview/index.ts +10 -0
  147. package/src/preview/types.ts +309 -0
  148. package/src/preview/validation.test.ts +290 -0
  149. package/src/preview/validation.ts +356 -0
  150. package/src/preview-runtime.tsx +144 -0
  151. package/src/raw-html-canonical.test.ts +41 -0
  152. package/src/raw-html-canonical.ts +179 -0
  153. package/src/react-types.ts +21 -0
  154. package/src/registry-install-plan.ts +344 -0
  155. package/src/registry.test.ts +245 -0
  156. package/src/registry.ts +369 -0
  157. package/src/rules/a11y-required-accessible-name.ts +93 -0
  158. package/src/rules/color-math.test.ts +72 -0
  159. package/src/rules/color-math.ts +192 -0
  160. package/src/rules/components-forbidden-prop-value.ts +108 -0
  161. package/src/rules/components-prefer-library.ts +943 -0
  162. package/src/rules/components-unknown-prop.ts +94 -0
  163. package/src/rules/composition-pattern-schema.ts +125 -0
  164. package/src/rules/composition-pattern.test.ts +320 -0
  165. package/src/rules/composition-pattern.ts +271 -0
  166. package/src/rules/emit-gate.test.ts +42 -0
  167. package/src/rules/emit-gate.ts +60 -0
  168. package/src/rules/finding.ts +54 -0
  169. package/src/rules/index.ts +235 -0
  170. package/src/rules/jsx-preferred-component.ts +68 -0
  171. package/src/rules/jsx-preferred-import-path.ts +67 -0
  172. package/src/rules/presets.ts +57 -0
  173. package/src/rules/props-invalid-value.ts +115 -0
  174. package/src/rules/rules.test.ts +3636 -0
  175. package/src/rules/spacing-resolution.ts +184 -0
  176. package/src/rules/styles-no-raw-color.ts +329 -0
  177. package/src/rules/styles-no-raw-dimensions.ts +357 -0
  178. package/src/rules/styles-no-raw-spacing.ts +236 -0
  179. package/src/rules/styles-no-raw-typography.ts +237 -0
  180. package/src/rules/tailwind-arbitrary-color.test.ts +128 -0
  181. package/src/rules/tailwind-arbitrary-color.ts +93 -0
  182. package/src/rules/tailwind-arbitrary-spacing.test.ts +157 -0
  183. package/src/rules/tailwind-arbitrary-spacing.ts +120 -0
  184. package/src/rules/tailwind-forbidden-palette.test.ts +116 -0
  185. package/src/rules/tailwind-forbidden-palette.ts +107 -0
  186. package/src/rules/tailwind-off-scale-spacing-token.test.ts +100 -0
  187. package/src/rules/tailwind-off-scale-spacing-token.ts +108 -0
  188. package/src/rules/tailwind-raw-color-via-token.test.ts +108 -0
  189. package/src/rules/tailwind-raw-color-via-token.ts +82 -0
  190. package/src/rules/tailwind-rule-utils.ts +106 -0
  191. package/src/rules/tailwind-unknown-class.test.ts +157 -0
  192. package/src/rules/tailwind-unknown-class.ts +121 -0
  193. package/src/rules/taxonomy.test.ts +238 -0
  194. package/src/rules/theme-no-theme-coupled-literal.ts +132 -0
  195. package/src/rules/tiers.ts +111 -0
  196. package/src/rules/tokens-css-vars-must-be-defined.test.ts +244 -0
  197. package/src/rules/tokens-css-vars-must-be-defined.ts +156 -0
  198. package/src/rules/tokens-require-dual-fallback.ts +78 -0
  199. package/src/rules/types.ts +27 -0
  200. package/src/rules/utils.test.ts +110 -0
  201. package/src/rules/utils.ts +599 -0
  202. package/src/schema.ts +427 -0
  203. package/src/schemas/index.ts +275 -0
  204. package/src/schemas/schemas.test.ts +124 -0
  205. package/src/severity.ts +54 -0
  206. package/src/storyAdapter.test.ts +571 -0
  207. package/src/storyAdapter.ts +761 -0
  208. package/src/storyFilters.test.ts +350 -0
  209. package/src/storyFilters.ts +253 -0
  210. package/src/storybook-csf.ts +11 -0
  211. package/src/style-comparison.ts +423 -0
  212. package/src/test-utils.ts +68 -0
  213. package/src/token-parser.ts +2 -0
  214. package/src/token-types.ts +287 -0
  215. package/src/tokens/categories.ts +131 -0
  216. package/src/tokens/color.ts +193 -0
  217. package/src/tokens/design-token-parser.test.ts +159 -0
  218. package/src/tokens/design-token-parser.ts +402 -0
  219. package/src/tokens/dtcg.ts +428 -0
  220. package/src/tokens/index.ts +130 -0
  221. package/src/tokens/lookup.ts +19 -0
  222. package/src/tokens/scss.ts +171 -0
  223. package/src/tokens/tailwind-v4.ts +187 -0
  224. package/src/tokens/types.ts +43 -0
  225. package/src/topology/index.ts +9 -0
  226. package/src/topology/resolve-area.test.ts +174 -0
  227. package/src/topology/resolve-area.ts +215 -0
  228. package/src/types.ts +1057 -0
@@ -0,0 +1,179 @@
1
+ export type RawHtmlPrecisionTier =
2
+ | "exact-html"
3
+ | "html-advisory"
4
+ | "input-type"
5
+ | "role-reimpl"
6
+ | "interactive-nonsemantic"
7
+ | "icon-only";
8
+
9
+ export interface RawHtmlCanonicalMatch {
10
+ canonical: string;
11
+ tier: RawHtmlPrecisionTier;
12
+ }
13
+
14
+ const EXACT_HTML_CANONICALS = new Map<string, string>([["button", "Button"]]);
15
+
16
+ const ADVISORY_HTML_CANONICALS = new Map<string, string>([
17
+ ["textarea", "Textarea"],
18
+ ["select", "Select"],
19
+ ["dialog", "Dialog"],
20
+ ["progress", "Progress"],
21
+ ["details", "Collapsible"],
22
+ ["table", "Table"],
23
+ ]);
24
+
25
+ const INPUT_TYPE_CANONICALS = new Map<string, string>([
26
+ ["checkbox", "Checkbox"],
27
+ ["radio", "Radio"],
28
+ ["number", "NumberInput"],
29
+ ["password", "PasswordInput"],
30
+ ["range", "Slider"],
31
+ ]);
32
+
33
+ const ROLE_CANONICALS = new Map<string, string>([
34
+ ["button", "Button"],
35
+ ["checkbox", "Checkbox"],
36
+ ["radio", "Radio"],
37
+ ["switch", "Switch"],
38
+ ["tab", "Tabs"],
39
+ ["tablist", "Tabs"],
40
+ ["dialog", "Dialog"],
41
+ ["menu", "Menu"],
42
+ ["status", "Alert"],
43
+ ]);
44
+
45
+ export const RAW_HTML_CANONICAL_TAGS = Object.freeze(
46
+ Array.from(EXACT_HTML_CANONICALS, ([tagName, canonical]) => ({
47
+ tagName,
48
+ canonical,
49
+ tier: "exact-html" as const,
50
+ }))
51
+ );
52
+
53
+ export const RAW_HTML_ADVISORY_TAGS = Object.freeze(
54
+ Array.from(ADVISORY_HTML_CANONICALS, ([tagName, canonical]) => ({
55
+ tagName,
56
+ canonical,
57
+ tier: "html-advisory" as const,
58
+ }))
59
+ );
60
+
61
+ export const RAW_HTML_INPUT_TYPE_CANONICALS = Object.freeze(
62
+ Array.from(INPUT_TYPE_CANONICALS, ([type, canonical]) => ({
63
+ type,
64
+ canonical,
65
+ tier: "input-type" as const,
66
+ }))
67
+ );
68
+
69
+ export const RAW_HTML_ROLE_CANONICALS = Object.freeze(
70
+ Array.from(ROLE_CANONICALS, ([role, canonical]) => ({
71
+ role,
72
+ canonical,
73
+ tier: "role-reimpl" as const,
74
+ }))
75
+ );
76
+
77
+ export function resolveCanonicalForRawHtml(
78
+ tagName: string,
79
+ inputType?: string
80
+ ): RawHtmlCanonicalMatch | null {
81
+ const tag = tagName.toLowerCase();
82
+ if (tag === "input") {
83
+ const normalizedType = inputType?.trim().toLowerCase();
84
+ if (!normalizedType) return null;
85
+ const canonical = INPUT_TYPE_CANONICALS.get(normalizedType);
86
+ return canonical ? { canonical, tier: "input-type" } : null;
87
+ }
88
+
89
+ const exactCanonical = EXACT_HTML_CANONICALS.get(tag);
90
+ if (exactCanonical) return { canonical: exactCanonical, tier: "exact-html" };
91
+ const advisoryCanonical = ADVISORY_HTML_CANONICALS.get(tag);
92
+ return advisoryCanonical ? { canonical: advisoryCanonical, tier: "html-advisory" } : null;
93
+ }
94
+
95
+ export function resolveCanonicalForAriaRole(role: string): RawHtmlCanonicalMatch | null {
96
+ const canonical = ROLE_CANONICALS.get(role.trim().toLowerCase());
97
+ return canonical ? { canonical, tier: "role-reimpl" } : null;
98
+ }
99
+
100
+ export function isRawHtmlAdvisoryTier(tier: RawHtmlPrecisionTier): boolean {
101
+ return (
102
+ tier === "html-advisory" ||
103
+ tier === "role-reimpl" ||
104
+ tier === "interactive-nonsemantic" ||
105
+ tier === "icon-only"
106
+ );
107
+ }
108
+
109
+ /**
110
+ * Low-frequency, typically-1:1 semantic tags where a per-component inferred
111
+ * `htmlEquivalent` mapping is safe even with a custom-named component
112
+ * (`<dialog>` → Modal). Mirrors `ADVISORY_HTML_CANONICALS` + the `input` family.
113
+ *
114
+ * Deliberately EXCLUDES generic structural containers (`div`, `span`) and the
115
+ * ambiguous common tags (`button`, `a`): those are handled by the stricter
116
+ * agreement check in `isEnforceableHtmlEquivalent`.
117
+ */
118
+ const SEMANTIC_HTML_EQUIVALENT_TAGS = new Set<string>([
119
+ "input",
120
+ "textarea",
121
+ "select",
122
+ "dialog",
123
+ "progress",
124
+ "details",
125
+ "table",
126
+ ]);
127
+
128
+ /**
129
+ * Specific interactive tags that an EXPLICITLY-DECLARED project mapping may
130
+ * enforce against ANY canonical component NAME — making the rule component-
131
+ * library agnostic (so `@acme/ui`'s `AcmeButton` fires on a raw `<button>`, not
132
+ * only the curated built-in "Button").
133
+ *
134
+ * Deliberately EXCLUDES generic structural containers (`div`, `span`): even an
135
+ * explicit declaration must never turn every `<div>` into a bespoke
136
+ * re-implementation. The `explicit` gate only applies to user-declared tag
137
+ * mappings (provenance: `localCanonical.resolves`), never to inferred
138
+ * `htmlEquivalent`s — so the `<button>` → Chip flood class stays name-gated.
139
+ */
140
+ const EXPLICITLY_ENFORCEABLE_AMBIGUOUS_TAGS = new Set<string>(["button", "a"]);
141
+
142
+ /**
143
+ * Decide whether a per-component `htmlEquivalent` mapping (raw `tag` → a
144
+ * `canonical` component) may drive a bare-tag canonical-usage finding.
145
+ *
146
+ * The contract:
147
+ * - Low-frequency SEMANTIC tags (dialog, select, textarea, input, …) are always
148
+ * enforceable, so a custom-named component (`<dialog>` → Modal) still maps.
149
+ * - Common/ambiguous tags (button, a, …) enforce ONLY when the mapping AGREES
150
+ * with the framework's curated canonical for that tag — so `<button>` → Button
151
+ * fires, but `<button>` → Chip does not.
152
+ * - Generic structural containers (div, span) have no curated answer and are not
153
+ * semantic → never enforce.
154
+ *
155
+ * This is the guard that stops the canonical-usage flood: a library component
156
+ * being built on a `<div>` must not make every `<div>` a bespoke re-implementation
157
+ * of it (every `<div>` → Accordion, every `<span>` → Badge, every `<button>` → Chip).
158
+ */
159
+ export function isEnforceableHtmlEquivalent(args: {
160
+ tag: string;
161
+ canonical: string;
162
+ inputType?: string;
163
+ /**
164
+ * The mapping is an EXPLICIT project declaration of this tag → component
165
+ * (provenance: `localCanonical.resolves`), not a low-confidence inferred
166
+ * `htmlEquivalent`. Explicit declarations enforce a specific interactive tag
167
+ * (`button`/`a`) against any canonical NAME — the agnosticism path — while
168
+ * inferred mappings stay name-gated to the curated canonical (the flood
169
+ * guard). Generic containers (`div`/`span`) are never enforceable even when
170
+ * explicit.
171
+ */
172
+ explicit?: boolean;
173
+ }): boolean {
174
+ const tag = args.tag.toLowerCase();
175
+ if (SEMANTIC_HTML_EQUIVALENT_TAGS.has(tag)) return true;
176
+ if (args.explicit && EXPLICITLY_ENFORCEABLE_AMBIGUOUS_TAGS.has(tag)) return true;
177
+ const builtIn = resolveCanonicalForRawHtml(tag, args.inputType);
178
+ return builtIn !== null && builtIn.canonical === args.canonical;
179
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * React-specific types for Fragments.
3
+ *
4
+ * Import from '@usefragments/core/react' when you need React-typed
5
+ * component definitions. The main '@usefragments/core' entrypoint
6
+ * works without React installed.
7
+ */
8
+
9
+ import type { ComponentType, ReactNode, JSX } from 'react';
10
+
11
+ /**
12
+ * A React component that can be used in a fragment definition.
13
+ * This type is intentionally broad to support various React component patterns
14
+ * including FC, forwardRef, memo, and class components across different React versions.
15
+ */
16
+ export type FragmentComponent<TProps = any> =
17
+ | ComponentType<TProps>
18
+ | ((props: TProps) => ReactNode | JSX.Element | null);
19
+
20
+ // Re-export definition types for convenience
21
+ export type { FragmentDefinition, FragmentDefinitionV2 } from './types.js';
@@ -0,0 +1,344 @@
1
+ import { portableRepoPathError } from "./config-paths.js";
2
+ import {
3
+ REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION,
4
+ hashRegistryFileContent,
5
+ registryInstallReceiptSchema,
6
+ type RegistryArtifact,
7
+ type RegistryDependency,
8
+ type RegistryFile,
9
+ type RegistryInstallReceipt,
10
+ } from "./registry.js";
11
+
12
+ export interface RegistryWritePlan {
13
+ path: string;
14
+ sourcePath: string;
15
+ content: string;
16
+ componentId?: string;
17
+ }
18
+
19
+ export interface BuildRegistryInstallPlanInput {
20
+ artifact: RegistryArtifact;
21
+ requestedComponents: readonly string[];
22
+ targetPath: string;
23
+ all?: boolean;
24
+ includeTests?: boolean;
25
+ }
26
+
27
+ export interface RegistryInstallPlan {
28
+ componentNames: string[];
29
+ plans: RegistryWritePlan[];
30
+ dependencies: RegistryDependency[];
31
+ }
32
+
33
+ export function normalizeRegistryRepoPath(path: string): string {
34
+ return path
35
+ .split("\\")
36
+ .join("/")
37
+ .replace(/^\.\/+/, "");
38
+ }
39
+
40
+ export function assertPortableRegistryPath(path: string, label: string): string {
41
+ const normalized = normalizeRegistryRepoPath(path);
42
+ const error = portableRepoPathError(normalized);
43
+ if (error) throw new Error(`${label}: ${error}`);
44
+ return normalized;
45
+ }
46
+
47
+ function joinRegistryPath(...parts: string[]): string {
48
+ return normalizeRegistryRepoPath(parts.filter(Boolean).join("/"));
49
+ }
50
+
51
+ export function sourceToRegistryTargetPath(targetRoot: string, sourcePath: string): string {
52
+ const source = normalizeRegistryRepoPath(sourcePath);
53
+ const withoutSrc = source.startsWith("src/") ? source.slice("src/".length) : source;
54
+ return assertPortableRegistryPath(
55
+ joinRegistryPath(targetRoot, withoutSrc),
56
+ "Registry install target"
57
+ );
58
+ }
59
+
60
+ export function selectedRegistryComponentNames(
61
+ artifact: RegistryArtifact,
62
+ requested: readonly string[],
63
+ all: boolean | undefined
64
+ ): string[] {
65
+ if (all || requested.includes("*")) {
66
+ return Object.keys(artifact.manifest.components).sort((left, right) =>
67
+ left.localeCompare(right)
68
+ );
69
+ }
70
+ if (requested.length === 0) {
71
+ throw new Error("Choose at least one registry component to install.");
72
+ }
73
+ for (const name of requested) {
74
+ if (!artifact.manifest.components[name]) {
75
+ throw new Error(`Unknown registry component ${name}`);
76
+ }
77
+ }
78
+ return [...new Set(requested)].sort();
79
+ }
80
+
81
+ function installsCompleteRegistry(
82
+ artifact: RegistryArtifact,
83
+ componentNames: readonly string[]
84
+ ): boolean {
85
+ const allComponents = Object.keys(artifact.manifest.components);
86
+ return (
87
+ componentNames.length === allComponents.length &&
88
+ allComponents.every((name) => componentNames.includes(name))
89
+ );
90
+ }
91
+
92
+ function shouldInstallFile(file: RegistryFile, includeTests: boolean | undefined): boolean {
93
+ if (includeTests) return true;
94
+ return file.role !== "test" && file.role !== "story";
95
+ }
96
+
97
+ function dependencyKey(dependency: RegistryDependency): string {
98
+ return `${dependency.type}:${dependency.name}:${dependency.versionRange}:${dependency.optional ? 1 : 0}`;
99
+ }
100
+
101
+ export function collectRegistryInstallDependencies(
102
+ artifact: RegistryArtifact,
103
+ componentNames: readonly string[]
104
+ ): RegistryDependency[] {
105
+ const dependencies = new Map<string, RegistryDependency>();
106
+ for (const dependency of artifact.manifest.dependencies) {
107
+ dependencies.set(dependencyKey(dependency), dependency);
108
+ }
109
+ for (const name of componentNames) {
110
+ const component = artifact.manifest.components[name];
111
+ for (const dependency of [...component.runtimeDependencies, ...component.peerDependencies]) {
112
+ dependencies.set(dependencyKey(dependency), dependency);
113
+ }
114
+ }
115
+ return [...dependencies.values()].sort((left, right) => left.name.localeCompare(right.name));
116
+ }
117
+
118
+ function collectComponentWritePlan(args: {
119
+ artifact: RegistryArtifact;
120
+ componentNames: readonly string[];
121
+ targetPath: string;
122
+ includeTests?: boolean;
123
+ }): RegistryWritePlan[] {
124
+ const writes = new Map<string, RegistryWritePlan>();
125
+ for (const componentName of args.componentNames) {
126
+ const component = args.artifact.manifest.components[componentName];
127
+ for (const file of component.files) {
128
+ if (!shouldInstallFile(file, args.includeTests)) continue;
129
+ const content = args.artifact.files[file.path];
130
+ if (!content) throw new Error(`Artifact is missing file content for ${file.path}`);
131
+ const target = sourceToRegistryTargetPath(args.targetPath, file.path);
132
+ writes.set(target, {
133
+ path: target,
134
+ sourcePath: file.path,
135
+ content: content.content,
136
+ componentId: component.componentId,
137
+ });
138
+ }
139
+ }
140
+ return [...writes.values()].sort((left, right) => left.path.localeCompare(right.path));
141
+ }
142
+
143
+ function rewriteInstalledPackagePath(value: string): string {
144
+ if (value.startsWith("./src/")) return `./${value.slice("./src/".length)}`;
145
+ if (value.startsWith("src/")) return value.slice("src/".length);
146
+ return value;
147
+ }
148
+
149
+ function rewriteInstalledPackageExport(value: unknown): unknown {
150
+ if (typeof value === "string") return rewriteInstalledPackagePath(value);
151
+ if (!value || typeof value !== "object" || Array.isArray(value)) return value;
152
+ return Object.fromEntries(
153
+ Object.entries(value).map(([key, nested]) => [key, rewriteInstalledPackageExport(nested)])
154
+ );
155
+ }
156
+
157
+ function rewriteInstalledPackageJson(content: string): string {
158
+ const parsed = JSON.parse(content) as {
159
+ name?: string;
160
+ version?: string;
161
+ type?: string;
162
+ sideEffects?: unknown;
163
+ main?: string;
164
+ types?: string;
165
+ fragments?: string;
166
+ exports?: unknown;
167
+ dependencies?: unknown;
168
+ peerDependencies?: unknown;
169
+ peerDependenciesMeta?: unknown;
170
+ };
171
+
172
+ const installed = {
173
+ ...(parsed.name && { name: parsed.name }),
174
+ ...(parsed.version && { version: parsed.version }),
175
+ private: true,
176
+ ...(parsed.type && { type: parsed.type }),
177
+ ...(parsed.sideEffects !== undefined && { sideEffects: parsed.sideEffects }),
178
+ ...(parsed.main && { main: rewriteInstalledPackagePath(parsed.main) }),
179
+ ...(parsed.types && { types: rewriteInstalledPackagePath(parsed.types) }),
180
+ fragments: rewriteInstalledPackagePath(parsed.fragments ?? "fragments.json"),
181
+ ...(parsed.exports !== undefined && {
182
+ exports: rewriteInstalledPackageExport(parsed.exports),
183
+ }),
184
+ ...(parsed.dependencies !== undefined && { dependencies: parsed.dependencies }),
185
+ ...(parsed.peerDependencies !== undefined && { peerDependencies: parsed.peerDependencies }),
186
+ ...(parsed.peerDependenciesMeta !== undefined && {
187
+ peerDependenciesMeta: parsed.peerDependenciesMeta,
188
+ }),
189
+ };
190
+
191
+ return `${JSON.stringify(installed, null, 2)}\n`;
192
+ }
193
+
194
+ function collectPackageWritePlan(args: {
195
+ artifact: RegistryArtifact;
196
+ targetPath: string;
197
+ includeTests?: boolean;
198
+ }): RegistryWritePlan[] {
199
+ const writes = new Map<string, RegistryWritePlan>();
200
+ const packageFiles = new Map(
201
+ args.artifact.manifest.packageFiles.map((file) => [file.path, file])
202
+ );
203
+ for (const entrypoint of args.artifact.manifest.entrypoints) {
204
+ const file = packageFiles.get(entrypoint.sourcePath);
205
+ if (file) continue;
206
+ const content = args.artifact.files[entrypoint.sourcePath];
207
+ if (!content) throw new Error(`Artifact is missing file content for ${entrypoint.sourcePath}`);
208
+ packageFiles.set(entrypoint.sourcePath, {
209
+ path: entrypoint.sourcePath,
210
+ role: "barrel",
211
+ sha256: content.sha256,
212
+ size: content.size,
213
+ contentType: content.contentType,
214
+ contentRef: `sha256:${content.sha256}`,
215
+ });
216
+ }
217
+
218
+ for (const file of packageFiles.values()) {
219
+ if (!shouldInstallFile(file, args.includeTests)) continue;
220
+ const content = args.artifact.files[file.path];
221
+ if (!content) throw new Error(`Artifact is missing file content for ${file.path}`);
222
+ const target = sourceToRegistryTargetPath(args.targetPath, file.path);
223
+ writes.set(target, {
224
+ path: target,
225
+ sourcePath: file.path,
226
+ content:
227
+ file.path === "package.json"
228
+ ? rewriteInstalledPackageJson(content.content)
229
+ : content.content,
230
+ });
231
+ }
232
+
233
+ return [...writes.values()].sort((left, right) => left.path.localeCompare(right.path));
234
+ }
235
+
236
+ function generatedIndexPlan(args: {
237
+ componentNames: readonly string[];
238
+ targetPath: string;
239
+ artifact: RegistryArtifact;
240
+ }): RegistryWritePlan {
241
+ const lines = args.componentNames.map((name) => {
242
+ const component = args.artifact.manifest.components[name];
243
+ const exportName = component.exports[0]?.name ?? component.name;
244
+ return `export { ${exportName} } from "./components/${component.componentId}";`;
245
+ });
246
+ return {
247
+ path: assertPortableRegistryPath(
248
+ joinRegistryPath(args.targetPath, "index.ts"),
249
+ "Registry index target"
250
+ ),
251
+ sourcePath: "registry/generated-index.ts",
252
+ content: `${lines.join("\n")}\n`,
253
+ };
254
+ }
255
+
256
+ function dedupePlans(plans: readonly RegistryWritePlan[]): RegistryWritePlan[] {
257
+ const byPath = new Map<string, RegistryWritePlan>();
258
+ for (const plan of plans) {
259
+ const existing = byPath.get(plan.path);
260
+ if (existing && existing.content !== plan.content) {
261
+ throw new Error(`Conflicting registry write plans for ${plan.path}`);
262
+ }
263
+ byPath.set(plan.path, existing ?? plan);
264
+ }
265
+ return [...byPath.values()].sort((left, right) => left.path.localeCompare(right.path));
266
+ }
267
+
268
+ export function buildRegistryInstallPlan(
269
+ input: BuildRegistryInstallPlanInput
270
+ ): RegistryInstallPlan {
271
+ const targetPath = assertPortableRegistryPath(input.targetPath, "Registry target");
272
+ const componentNames = selectedRegistryComponentNames(
273
+ input.artifact,
274
+ input.requestedComponents,
275
+ input.all
276
+ );
277
+ const dependencies = collectRegistryInstallDependencies(input.artifact, componentNames);
278
+ const isCompleteInstall = installsCompleteRegistry(input.artifact, componentNames);
279
+ const hasPackageRoot = input.artifact.manifest.entrypoints.some(
280
+ (entrypoint) => entrypoint.specifier === "."
281
+ );
282
+ const plans = dedupePlans([
283
+ ...(isCompleteInstall
284
+ ? collectPackageWritePlan({
285
+ artifact: input.artifact,
286
+ targetPath,
287
+ includeTests: input.includeTests,
288
+ })
289
+ : []),
290
+ ...collectComponentWritePlan({
291
+ artifact: input.artifact,
292
+ componentNames,
293
+ targetPath,
294
+ includeTests: input.includeTests,
295
+ }),
296
+ ...(isCompleteInstall && hasPackageRoot
297
+ ? []
298
+ : [generatedIndexPlan({ artifact: input.artifact, componentNames, targetPath })]),
299
+ ]);
300
+
301
+ return { componentNames, plans, dependencies };
302
+ }
303
+
304
+ export function buildRegistryInstallReceipt(args: {
305
+ artifact: RegistryArtifact;
306
+ targetPath: string;
307
+ importPath: string;
308
+ componentNames: readonly string[];
309
+ plans: readonly RegistryWritePlan[];
310
+ dependencies: readonly RegistryDependency[];
311
+ installedAt?: string;
312
+ }): RegistryInstallReceipt {
313
+ const installedComponents = args.componentNames.map((name) => {
314
+ const component = args.artifact.manifest.components[name];
315
+ return {
316
+ componentId: component.componentId,
317
+ publicRef: component.publicRef,
318
+ name: component.name,
319
+ files: args.plans
320
+ .filter((plan) => plan.componentId === component.componentId)
321
+ .map((plan) => plan.path),
322
+ };
323
+ });
324
+
325
+ return registryInstallReceiptSchema.parse({
326
+ schemaVersion: REGISTRY_INSTALL_RECEIPT_SCHEMA_VERSION,
327
+ registryId: args.artifact.manifest.registryId,
328
+ registryVersion: args.artifact.manifest.version,
329
+ registryHash: args.artifact.manifest.registryHash,
330
+ fcid: args.artifact.manifest.fcid,
331
+ channel: args.artifact.manifest.channel,
332
+ targetPath: assertPortableRegistryPath(args.targetPath, "Registry target"),
333
+ importPath: args.importPath,
334
+ installedComponents,
335
+ files: args.plans.map((plan) => ({
336
+ path: plan.path,
337
+ sourcePath: plan.sourcePath,
338
+ sha256: hashRegistryFileContent(plan.content),
339
+ componentId: plan.componentId,
340
+ })),
341
+ dependencies: args.dependencies,
342
+ installedAt: args.installedAt ?? new Date().toISOString(),
343
+ });
344
+ }