@usefragments/core 1.10.2 → 2.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 (130) hide show
  1. package/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
  2. package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
  3. package/dist/chunk-7ULGH74M.js +66 -0
  4. package/dist/chunk-7ULGH74M.js.map +1 -0
  5. package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
  6. package/dist/chunk-BMPYIUZE.js.map +1 -0
  7. package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
  8. package/dist/chunk-ML5S6QNU.js.map +1 -0
  9. package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
  10. package/dist/chunk-MZ2FS7U4.js.map +1 -0
  11. package/dist/chunk-PWIJMOI4.js +202 -0
  12. package/dist/chunk-PWIJMOI4.js.map +1 -0
  13. package/dist/chunk-RYFULE43.js +578 -0
  14. package/dist/chunk-RYFULE43.js.map +1 -0
  15. package/dist/codes/index.d.ts +2 -2
  16. package/dist/codes/index.js +3 -2
  17. package/dist/compiled-types/index.d.ts +327 -2
  18. package/dist/compiled-types/index.js +1 -1
  19. package/dist/generate/index.d.ts +3 -2
  20. package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
  21. package/dist/governance-telemetry.d.ts +6 -0
  22. package/dist/governance-telemetry.js +1 -1
  23. package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
  24. package/dist/index.d.ts +3280 -1305
  25. package/dist/index.js +2414 -350
  26. package/dist/index.js.map +1 -1
  27. package/dist/manifest.d.ts +228 -0
  28. package/dist/manifest.js +24 -0
  29. package/dist/manifest.js.map +1 -0
  30. package/dist/preview/index.js +45 -1
  31. package/dist/preview/index.js.map +1 -1
  32. package/dist/preview-runtime.d.ts +1 -2
  33. package/dist/preview-runtime.js +150 -14
  34. package/dist/preview-runtime.js.map +1 -1
  35. package/dist/react-types.d.ts +1 -2
  36. package/dist/registry.d.ts +1412 -203
  37. package/dist/registry.js +30 -3
  38. package/dist/schemas/index.d.ts +1 -1
  39. package/dist/schemas/index.js +3 -2
  40. package/dist/storyAdapter.d.ts +1 -2
  41. package/dist/storyAdapter.js +11 -49
  42. package/dist/storyAdapter.js.map +1 -1
  43. package/dist/test-utils.d.ts +3 -2
  44. package/dist/topology/index.d.ts +1 -1
  45. package/dist/topology/index.js +1 -1
  46. package/package.json +8 -2
  47. package/src/__tests__/contract-parser.test.ts +318 -277
  48. package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
  49. package/src/__tests__/preview-runtime.test.tsx +30 -8
  50. package/src/__tests__/schema.test.ts +191 -14
  51. package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
  52. package/src/analysis-plan/coverage.ts +181 -0
  53. package/src/analysis-plan/digest.ts +141 -0
  54. package/src/analysis-plan/index.ts +34 -0
  55. package/src/analysis-plan/types.ts +207 -0
  56. package/src/approved-contract-tokens.test.ts +39 -0
  57. package/src/approved-contract-tokens.ts +18 -0
  58. package/src/codes/__tests__/codes.test.ts +13 -0
  59. package/src/codes/codes.ts +40 -0
  60. package/src/compiled-types/index.ts +640 -39
  61. package/src/compiled-types/parse.test.ts +145 -4
  62. package/src/component-contract.ts +95 -53
  63. package/src/composition.ts +7 -13
  64. package/src/constants.ts +3 -6
  65. package/src/contract/hash.test.ts +20 -0
  66. package/src/contract/hash.ts +66 -9
  67. package/src/contract/index.ts +24 -1
  68. package/src/contract/manifest.test.ts +94 -0
  69. package/src/contract/manifest.ts +68 -0
  70. package/src/contract/preimage.test.ts +219 -1
  71. package/src/contract/preimage.ts +326 -6
  72. package/src/contract/stamp.test.ts +3 -0
  73. package/src/contract/stamp.ts +1 -1
  74. package/src/contract-parser.ts +54 -30
  75. package/src/defineFragment.test.ts +476 -91
  76. package/src/defineFragment.ts +204 -114
  77. package/src/domain-ids.test.ts +35 -0
  78. package/src/domain-ids.ts +61 -0
  79. package/src/evaluation/evaluate.test.ts +522 -0
  80. package/src/evaluation/evaluate.ts +690 -0
  81. package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
  82. package/src/evaluation/index.ts +58 -0
  83. package/src/evaluation/receipt.ts +753 -0
  84. package/src/evaluation/types.ts +406 -0
  85. package/src/facts/builders.ts +2 -0
  86. package/src/facts/compile.ts +29 -6
  87. package/src/facts/fact-index.ts +13 -3
  88. package/src/facts/fact-integrity-v1.test.ts +172 -0
  89. package/src/facts/facts.test.ts +15 -0
  90. package/src/facts/ids.ts +46 -3
  91. package/src/facts/index.ts +14 -1
  92. package/src/facts/integrity.ts +134 -0
  93. package/src/facts/types.ts +36 -0
  94. package/src/governance-integrity.test.ts +1 -0
  95. package/src/governance-integrity.ts +5 -3
  96. package/src/governance-telemetry.ts +8 -0
  97. package/src/governance.ts +70 -8
  98. package/src/index.ts +230 -37
  99. package/src/preview/validation.test.ts +62 -0
  100. package/src/preview/validation.ts +48 -2
  101. package/src/preview-runtime.tsx +227 -20
  102. package/src/registry-install-plan.ts +200 -109
  103. package/src/registry-shards.test.ts +263 -0
  104. package/src/registry.ts +237 -0
  105. package/src/repository-binding.test.ts +50 -0
  106. package/src/repository-binding.ts +96 -0
  107. package/src/rules/families.test.ts +36 -0
  108. package/src/rules/finding.ts +7 -2
  109. package/src/rules/index.ts +17 -1
  110. package/src/rules/rule-config.test.ts +66 -0
  111. package/src/rules/rule-config.ts +73 -0
  112. package/src/rules/rules.test.ts +26 -0
  113. package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
  114. package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
  115. package/src/schema.ts +293 -113
  116. package/src/schemas/index.ts +1 -1
  117. package/src/storyAdapter.test.ts +68 -12
  118. package/src/storyAdapter.ts +44 -75
  119. package/src/topology/resolve-area.ts +1 -1
  120. package/src/types.ts +258 -40
  121. package/dist/chunk-3LLRNCPX.js.map +0 -1
  122. package/dist/chunk-RANPUC6C.js +0 -72
  123. package/dist/chunk-RANPUC6C.js.map +0 -1
  124. package/dist/chunk-XN3LSDPY.js.map +0 -1
  125. package/dist/chunk-YF65VYRY.js.map +0 -1
  126. package/src/fragment-types.ts +0 -214
  127. package/src/react-create-element.test.ts +0 -22
  128. package/src/react-create-element.ts +0 -12
  129. package/src/storyFilters.test.ts +0 -350
  130. package/src/storyFilters.ts +0 -253
@@ -7,6 +7,7 @@ import {
7
7
  type RegistryDependency,
8
8
  type RegistryFile,
9
9
  type RegistryInstallReceipt,
10
+ type RegistryManifest,
10
11
  } from "./registry.js";
11
12
 
12
13
  export interface RegistryWritePlan {
@@ -16,6 +17,40 @@ export interface RegistryWritePlan {
16
17
  componentId?: string;
17
18
  }
18
19
 
20
+ export interface RegistryInstallSelectionInput {
21
+ manifest: RegistryManifest;
22
+ requestedComponents: readonly string[];
23
+ all?: boolean;
24
+ }
25
+
26
+ export interface RegistryInstallSelection {
27
+ /** Component ids as resolved from the request (canonical casing), sorted. */
28
+ requested: string[];
29
+ /** Components pulled in through `internalDependencies`, sorted; never overlaps `requested`. */
30
+ implied: string[];
31
+ /** The full install set: `requested` ∪ `implied`, sorted. */
32
+ components: string[];
33
+ }
34
+
35
+ export interface RegistryInstallSourcesInput extends RegistryInstallSelectionInput {
36
+ includeTests?: boolean;
37
+ }
38
+
39
+ export interface RegistryInstallSourceFile {
40
+ file: RegistryFile;
41
+ componentId?: string;
42
+ }
43
+
44
+ export interface RegistryInstallSources {
45
+ selection: RegistryInstallSelection;
46
+ /** True when the selection covers every component (package entrypoints are installed too). */
47
+ complete: boolean;
48
+ /** Source files the install writes, sorted by path and unique. */
49
+ files: RegistryInstallSourceFile[];
50
+ /** Registry file paths the install needs bodies for (includes entrypoint-only paths). */
51
+ paths: string[];
52
+ }
53
+
19
54
  export interface BuildRegistryInstallPlanInput {
20
55
  artifact: RegistryArtifact;
21
56
  requestedComponents: readonly string[];
@@ -26,6 +61,8 @@ export interface BuildRegistryInstallPlanInput {
26
61
 
27
62
  export interface RegistryInstallPlan {
28
63
  componentNames: string[];
64
+ requestedComponents: string[];
65
+ impliedComponents: string[];
29
66
  plans: RegistryWritePlan[];
30
67
  dependencies: RegistryDependency[];
31
68
  }
@@ -48,6 +85,10 @@ function joinRegistryPath(...parts: string[]): string {
48
85
  return normalizeRegistryRepoPath(parts.filter(Boolean).join("/"));
49
86
  }
50
87
 
88
+ function sortStrings(values: Iterable<string>): string[] {
89
+ return [...new Set(values)].sort((left, right) => left.localeCompare(right));
90
+ }
91
+
51
92
  export function sourceToRegistryTargetPath(targetRoot: string, sourcePath: string): string {
52
93
  const source = normalizeRegistryRepoPath(sourcePath);
53
94
  const withoutSrc = source.startsWith("src/") ? source.slice("src/".length) : source;
@@ -57,54 +98,100 @@ export function sourceToRegistryTargetPath(targetRoot: string, sourcePath: strin
57
98
  );
58
99
  }
59
100
 
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)
101
+ /**
102
+ * Resolve a requested component id to its canonical id. Exact matches win; otherwise a
103
+ * unique case-insensitive match is accepted (`button` → `Button`).
104
+ */
105
+ export function resolveRegistryComponentId(manifest: RegistryManifest, requested: string): string {
106
+ if (manifest.components[requested]) return requested;
107
+ const lowered = requested.toLowerCase();
108
+ const matches = Object.keys(manifest.components).filter((name) => name.toLowerCase() === lowered);
109
+ if (matches.length === 1) return matches[0];
110
+ if (matches.length > 1) {
111
+ throw new Error(
112
+ `Ambiguous registry component ${requested}: matches ${matches.sort().join(", ")}`
68
113
  );
69
114
  }
70
- if (requested.length === 0) {
71
- throw new Error("Choose at least one registry component to install.");
115
+ throw new Error(`Unknown registry component ${requested}`);
116
+ }
117
+
118
+ /** Close a component set over `internalDependencies`, transitively. */
119
+ export function expandRegistryComponentClosure(
120
+ manifest: RegistryManifest,
121
+ names: readonly string[]
122
+ ): string[] {
123
+ const closure = new Set<string>();
124
+ const queue = [...names];
125
+ while (queue.length > 0) {
126
+ const name = queue.shift()!;
127
+ if (closure.has(name)) continue;
128
+ const component = manifest.components[name];
129
+ if (!component) throw new Error(`Unknown registry component ${name}`);
130
+ closure.add(name);
131
+ queue.push(...component.internalDependencies);
72
132
  }
73
- for (const name of requested) {
74
- if (!artifact.manifest.components[name]) {
75
- throw new Error(`Unknown registry component ${name}`);
76
- }
133
+ return sortStrings(closure);
134
+ }
135
+
136
+ export function resolveRegistryInstallSelection(
137
+ input: RegistryInstallSelectionInput
138
+ ): RegistryInstallSelection {
139
+ const { manifest } = input;
140
+ if (input.all || input.requestedComponents.includes("*")) {
141
+ const components = sortStrings(Object.keys(manifest.components));
142
+ return { requested: components, implied: [], components };
77
143
  }
78
- return [...new Set(requested)].sort();
144
+ if (input.requestedComponents.length === 0) {
145
+ throw new Error("Choose at least one registry component to install.");
146
+ }
147
+ const requested = sortStrings(
148
+ input.requestedComponents.map((name) => resolveRegistryComponentId(manifest, name))
149
+ );
150
+ const components = expandRegistryComponentClosure(manifest, requested);
151
+ const requestedSet = new Set(requested);
152
+ return {
153
+ requested,
154
+ implied: components.filter((name) => !requestedSet.has(name)),
155
+ components,
156
+ };
79
157
  }
80
158
 
81
159
  function installsCompleteRegistry(
82
- artifact: RegistryArtifact,
160
+ manifest: RegistryManifest,
83
161
  componentNames: readonly string[]
84
162
  ): boolean {
85
- const allComponents = Object.keys(artifact.manifest.components);
163
+ const allComponents = Object.keys(manifest.components);
86
164
  return (
87
165
  componentNames.length === allComponents.length &&
88
166
  allComponents.every((name) => componentNames.includes(name))
89
167
  );
90
168
  }
91
169
 
170
+ /** Authoring-only roles: shipped in the artifact, written only with `includeTests`. */
171
+ const AUTHORING_ROLES: ReadonlySet<RegistryFile["role"]> = new Set(["test", "story", "example"]);
172
+
92
173
  function shouldInstallFile(file: RegistryFile, includeTests: boolean | undefined): boolean {
93
174
  if (includeTests) return true;
94
- return file.role !== "test" && file.role !== "story";
175
+ return !AUTHORING_ROLES.has(file.role);
95
176
  }
96
177
 
97
178
  function dependencyKey(dependency: RegistryDependency): string {
98
179
  return `${dependency.type}:${dependency.name}:${dependency.versionRange}:${dependency.optional ? 1 : 0}`;
99
180
  }
100
181
 
182
+ /**
183
+ * npm dependencies the install needs: the selected components' own dependencies, plus the
184
+ * whole package's list when the install is complete (package entrypoints ship too).
185
+ */
101
186
  export function collectRegistryInstallDependencies(
102
- artifact: RegistryArtifact,
187
+ artifact: Pick<RegistryArtifact, "manifest">,
103
188
  componentNames: readonly string[]
104
189
  ): RegistryDependency[] {
105
190
  const dependencies = new Map<string, RegistryDependency>();
106
- for (const dependency of artifact.manifest.dependencies) {
107
- dependencies.set(dependencyKey(dependency), dependency);
191
+ if (installsCompleteRegistry(artifact.manifest, componentNames)) {
192
+ for (const dependency of artifact.manifest.dependencies) {
193
+ dependencies.set(dependencyKey(dependency), dependency);
194
+ }
108
195
  }
109
196
  for (const name of componentNames) {
110
197
  const component = artifact.manifest.components[name];
@@ -115,29 +202,53 @@ export function collectRegistryInstallDependencies(
115
202
  return [...dependencies.values()].sort((left, right) => left.name.localeCompare(right.name));
116
203
  }
117
204
 
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
- });
205
+ function packageFileEntries(manifest: RegistryManifest): Map<string, RegistryFile | null> {
206
+ const packageFiles = new Map<string, RegistryFile | null>(
207
+ manifest.packageFiles.map((file) => [file.path, file])
208
+ );
209
+ for (const entrypoint of manifest.entrypoints) {
210
+ if (!packageFiles.has(entrypoint.sourcePath)) packageFiles.set(entrypoint.sourcePath, null);
211
+ }
212
+ return packageFiles;
213
+ }
214
+
215
+ /**
216
+ * Everything an install needs from the registry, computed from the manifest alone so a
217
+ * sharded reader can fetch only the bodies the plan will write.
218
+ */
219
+ export function resolveRegistryInstallSources(
220
+ input: RegistryInstallSourcesInput
221
+ ): RegistryInstallSources {
222
+ const { manifest } = input;
223
+ const selection = resolveRegistryInstallSelection(input);
224
+ const complete = installsCompleteRegistry(manifest, selection.components);
225
+ const files = new Map<string, RegistryInstallSourceFile>();
226
+ const paths = new Set<string>();
227
+ const add = (file: RegistryFile, componentId?: string) => {
228
+ if (!shouldInstallFile(file, input.includeTests)) return;
229
+ if (!files.has(file.path)) files.set(file.path, { file, componentId });
230
+ paths.add(file.path);
231
+ };
232
+
233
+ if (complete) {
234
+ for (const [path, file] of packageFileEntries(manifest)) {
235
+ if (file) add(file);
236
+ else paths.add(path);
138
237
  }
238
+ } else {
239
+ for (const file of manifest.baseFiles) add(file);
139
240
  }
140
- return [...writes.values()].sort((left, right) => left.path.localeCompare(right.path));
241
+ for (const name of selection.components) {
242
+ const component = manifest.components[name];
243
+ for (const file of component.files) add(file, component.componentId);
244
+ }
245
+
246
+ return {
247
+ selection,
248
+ complete,
249
+ files: [...files.values()].sort((left, right) => left.file.path.localeCompare(right.file.path)),
250
+ paths: sortStrings(paths),
251
+ };
141
252
  }
142
253
 
143
254
  function rewriteInstalledPackagePath(value: string): string {
@@ -191,55 +302,19 @@ function rewriteInstalledPackageJson(content: string): string {
191
302
  return `${JSON.stringify(installed, null, 2)}\n`;
192
303
  }
193
304
 
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));
305
+ function contentFor(artifact: RegistryArtifact, path: string): string {
306
+ const content = artifact.files[path];
307
+ if (!content) throw new Error(`Artifact is missing file content for ${path}`);
308
+ return content.content;
234
309
  }
235
310
 
236
311
  function generatedIndexPlan(args: {
237
312
  componentNames: readonly string[];
238
313
  targetPath: string;
239
- artifact: RegistryArtifact;
314
+ manifest: RegistryManifest;
240
315
  }): RegistryWritePlan {
241
316
  const lines = args.componentNames.map((name) => {
242
- const component = args.artifact.manifest.components[name];
317
+ const component = args.manifest.components[name];
243
318
  const exportName = component.exports[0]?.name ?? component.name;
244
319
  return `export { ${exportName} } from "./components/${component.componentId}";`;
245
320
  });
@@ -268,37 +343,53 @@ function dedupePlans(plans: readonly RegistryWritePlan[]): RegistryWritePlan[] {
268
343
  export function buildRegistryInstallPlan(
269
344
  input: BuildRegistryInstallPlanInput
270
345
  ): RegistryInstallPlan {
346
+ const { manifest } = input.artifact;
271
347
  const targetPath = assertPortableRegistryPath(input.targetPath, "Registry target");
272
- const componentNames = selectedRegistryComponentNames(
273
- input.artifact,
274
- input.requestedComponents,
275
- input.all
276
- );
348
+ const sources = resolveRegistryInstallSources({
349
+ manifest,
350
+ requestedComponents: input.requestedComponents,
351
+ all: input.all,
352
+ includeTests: input.includeTests,
353
+ });
354
+ const componentNames = sources.selection.components;
277
355
  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
- );
356
+ const hasPackageRoot = manifest.entrypoints.some((entrypoint) => entrypoint.specifier === ".");
357
+
358
+ const writes: RegistryWritePlan[] = sources.files.map(({ file, componentId }) => {
359
+ const raw = contentFor(input.artifact, file.path);
360
+ return {
361
+ path: sourceToRegistryTargetPath(targetPath, file.path),
362
+ sourcePath: file.path,
363
+ content:
364
+ sources.complete && file.path === "package.json" ? rewriteInstalledPackageJson(raw) : raw,
365
+ ...(componentId && { componentId }),
366
+ };
367
+ });
368
+ if (sources.complete) {
369
+ for (const entrypoint of manifest.entrypoints) {
370
+ if (writes.some((plan) => plan.sourcePath === entrypoint.sourcePath)) continue;
371
+ writes.push({
372
+ path: sourceToRegistryTargetPath(targetPath, entrypoint.sourcePath),
373
+ sourcePath: entrypoint.sourcePath,
374
+ content: contentFor(input.artifact, entrypoint.sourcePath),
375
+ });
376
+ }
377
+ }
378
+
282
379
  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
380
+ ...writes,
381
+ ...(sources.complete && hasPackageRoot
297
382
  ? []
298
- : [generatedIndexPlan({ artifact: input.artifact, componentNames, targetPath })]),
383
+ : [generatedIndexPlan({ manifest, componentNames, targetPath })]),
299
384
  ]);
300
385
 
301
- return { componentNames, plans, dependencies };
386
+ return {
387
+ componentNames,
388
+ requestedComponents: sources.selection.requested,
389
+ impliedComponents: sources.selection.implied,
390
+ plans,
391
+ dependencies,
392
+ };
302
393
  }
303
394
 
304
395
  export function buildRegistryInstallReceipt(args: {
@@ -0,0 +1,263 @@
1
+ import { describe, expect, it } from "vitest";
2
+
3
+ import {
4
+ REGISTRY_INDEX_SCHEMA_VERSION,
5
+ REGISTRY_MANIFEST_SCHEMA_VERSION,
6
+ REGISTRY_POINTER_SCHEMA_VERSION,
7
+ REGISTRY_SHARD_SCHEMA_VERSION,
8
+ assembleRegistryArtifact,
9
+ buildRegistryArtifact,
10
+ buildRegistryFile,
11
+ buildRegistryPointer,
12
+ defaultRegistryShardPath,
13
+ registryIndexSchema,
14
+ registryPointerSchema,
15
+ registryShardSchema,
16
+ registryShardsForPaths,
17
+ shardRegistryArtifact,
18
+ type RegistryArtifact,
19
+ type RegistryManifestDraft,
20
+ } from "./registry.js";
21
+ import {
22
+ buildRegistryInstallPlan,
23
+ resolveRegistryComponentId,
24
+ resolveRegistryInstallSelection,
25
+ resolveRegistryInstallSources,
26
+ } from "./registry-install-plan.js";
27
+
28
+ function file(path: string, content: string, role: "source" | "style" | "token" | "test") {
29
+ return buildRegistryFile({ path, role, content, contentType: "text/plain" });
30
+ }
31
+
32
+ function kit(): RegistryArtifact {
33
+ const index = file("src/index.ts", 'export { Button } from "./components/Button";\n', "source");
34
+ const globals = file("src/styles/globals.scss", '@use "../tokens/variables";\n', "style");
35
+ const variables = file("src/tokens/_variables.scss", ":root { --fui-x: 1; }\n", "token");
36
+ const button = file("src/components/Button/index.tsx", "export const Button = 1;\n", "source");
37
+ const buttonStyle = file("src/components/Button/Button.module.scss", ".b {}\n", "style");
38
+ const buttonTest = file("src/components/Button/Button.test.tsx", "test\n", "test");
39
+ const text = file("src/components/Text/index.tsx", "export const Text = 1;\n", "source");
40
+ const card = file("src/components/Card/index.tsx", "export const Card = 1;\n", "source");
41
+ const util = file("src/utils/label.ts", "export const label = 1;\n", "source");
42
+
43
+ const component = (
44
+ componentId: string,
45
+ files: Array<{ metadata: ReturnType<typeof file>["metadata"] }>,
46
+ internalDependencies: string[] = []
47
+ ) => ({
48
+ componentId,
49
+ publicRef: `acme-ui:${componentId}`,
50
+ name: componentId,
51
+ files: files.map((entry) => entry.metadata),
52
+ internalDependencies,
53
+ runtimeDependencies: [
54
+ { name: `dep-${componentId}`, versionRange: "^1", type: "runtime" as const },
55
+ ],
56
+ });
57
+
58
+ const manifest: RegistryManifestDraft = {
59
+ schemaVersion: REGISTRY_MANIFEST_SCHEMA_VERSION,
60
+ registryId: "acme-ui",
61
+ name: "Acme UI",
62
+ channel: "stable",
63
+ version: "1.0.0",
64
+ generatedAt: "2026-06-14T12:00:00.000Z",
65
+ source: { kind: "manual", label: "fixture" },
66
+ packageFiles: [index.metadata],
67
+ baseFiles: [globals.metadata, variables.metadata],
68
+ entrypoints: [{ specifier: ".", sourcePath: "src/index.ts" }],
69
+ components: {
70
+ Button: component("Button", [button, buttonStyle, buttonTest, variables, util], ["Text"]),
71
+ Text: component("Text", [text]),
72
+ Card: component("Card", [card], ["Text"]),
73
+ },
74
+ dependencies: [{ name: "shared-dep", versionRange: "^1", type: "runtime" }],
75
+ };
76
+
77
+ return buildRegistryArtifact({
78
+ manifest,
79
+ files: [index, globals, variables, button, buttonStyle, buttonTest, text, card, util].map(
80
+ (entry) => entry.content
81
+ ),
82
+ });
83
+ }
84
+
85
+ describe("sharded registry artifact", () => {
86
+ it("lays out shards per component and per shared directory", () => {
87
+ expect(defaultRegistryShardPath("src/components/Button/index.tsx")).toBe(
88
+ "components/Button.json"
89
+ );
90
+ expect(defaultRegistryShardPath("src/tokens/_variables.scss")).toBe("shared/tokens.json");
91
+ expect(defaultRegistryShardPath("src/scss.d.ts")).toBe("shared/src.json");
92
+ expect(defaultRegistryShardPath("package.json")).toBe("shared/package.json");
93
+ expect(defaultRegistryShardPath("fragments.json")).toBe("shared/package.json");
94
+ });
95
+
96
+ it("splits an artifact into an index and shards that assemble back losslessly", () => {
97
+ const artifact = kit();
98
+ const { index, shards } = shardRegistryArtifact(artifact);
99
+
100
+ expect(index.schemaVersion).toBe(REGISTRY_INDEX_SCHEMA_VERSION);
101
+ expect(index.manifest).toEqual(artifact.manifest);
102
+ expect(Object.keys(index.shards).sort()).toEqual([
103
+ "components/Button.json",
104
+ "components/Card.json",
105
+ "components/Text.json",
106
+ "shared/src.json",
107
+ "shared/styles.json",
108
+ "shared/tokens.json",
109
+ "shared/utils.json",
110
+ ]);
111
+ for (const shard of shards.values()) {
112
+ expect(shard.schemaVersion).toBe(REGISTRY_SHARD_SCHEMA_VERSION);
113
+ expect(() => registryShardSchema.parse(shard)).not.toThrow();
114
+ }
115
+ expect(() => registryIndexSchema.parse(index)).not.toThrow();
116
+
117
+ const paths = Object.keys(artifact.files);
118
+ const assembled = assembleRegistryArtifact({ index, shards, paths });
119
+ expect(assembled.files).toEqual(artifact.files);
120
+ expect(assembled.manifest.registryHash).toBe(artifact.manifest.registryHash);
121
+ });
122
+
123
+ it("names only the shards a path set needs and rejects unknown paths", () => {
124
+ const { index } = shardRegistryArtifact(kit());
125
+ expect(
126
+ registryShardsForPaths(index, ["src/components/Button/index.tsx", "src/utils/label.ts"])
127
+ ).toEqual(["components/Button.json", "shared/utils.json"]);
128
+ expect(() => registryShardsForPaths(index, ["src/nope.ts"])).toThrow(/no shard for src\/nope/);
129
+ });
130
+
131
+ it("refuses shard content that does not match the index", () => {
132
+ const { index, shards } = shardRegistryArtifact(kit());
133
+ const tampered = new Map(shards);
134
+ const button = structuredClone(tampered.get("components/Button.json")!);
135
+ for (const entry of Object.values(button.files)) entry.content = "evil";
136
+ tampered.set("components/Button.json", button);
137
+
138
+ expect(() =>
139
+ assembleRegistryArtifact({
140
+ index,
141
+ shards: tampered,
142
+ paths: ["src/components/Button/index.tsx"],
143
+ })
144
+ ).toThrow(/does not match the index/);
145
+ expect(() =>
146
+ assembleRegistryArtifact({
147
+ index,
148
+ shards: new Map(),
149
+ paths: ["src/components/Button/index.tsx"],
150
+ })
151
+ ).toThrow(/was not loaded/);
152
+ });
153
+
154
+ it("rejects an index that lists a manifest file in no shard or in two", () => {
155
+ const { index } = shardRegistryArtifact(kit());
156
+ const missing = structuredClone(index);
157
+ delete missing.shards["shared/utils.json"];
158
+ expect(() => registryIndexSchema.parse(missing)).toThrow(/not carried by any shard/);
159
+
160
+ const duplicated = structuredClone(index);
161
+ duplicated.shards["shared/tokens.json"].push("src/utils/label.ts");
162
+ expect(() => registryIndexSchema.parse(duplicated)).toThrow(/listed in both/);
163
+ });
164
+
165
+ it("builds a latest.json pointer that names the versioned index by relative path", () => {
166
+ const { index } = shardRegistryArtifact(kit());
167
+ const pointer = buildRegistryPointer(index, "1.0.0/index.json");
168
+ expect(pointer).toEqual({
169
+ schemaVersion: REGISTRY_POINTER_SCHEMA_VERSION,
170
+ registryId: "acme-ui",
171
+ version: "1.0.0",
172
+ registryHash: index.manifest.registryHash,
173
+ index: "1.0.0/index.json",
174
+ generatedAt: "2026-06-14T12:00:00.000Z",
175
+ });
176
+ expect(() =>
177
+ registryPointerSchema.parse({ ...pointer, index: "https://x/index.json" })
178
+ ).toThrow();
179
+ expect(() => registryPointerSchema.parse({ ...pointer, index: "../index.json" })).toThrow();
180
+ });
181
+ });
182
+
183
+ describe("registry install selection", () => {
184
+ it("resolves component ids case-insensitively and closes over internal dependencies", () => {
185
+ const { manifest } = kit();
186
+ expect(resolveRegistryComponentId(manifest, "button")).toBe("Button");
187
+ expect(resolveRegistryComponentId(manifest, "Button")).toBe("Button");
188
+ expect(() => resolveRegistryComponentId(manifest, "nope")).toThrow(
189
+ /Unknown registry component/
190
+ );
191
+
192
+ const selection = resolveRegistryInstallSelection({
193
+ manifest,
194
+ requestedComponents: ["card", "BUTTON"],
195
+ });
196
+ expect(selection).toEqual({
197
+ requested: ["Button", "Card"],
198
+ implied: ["Text"],
199
+ components: ["Button", "Card", "Text"],
200
+ });
201
+ });
202
+
203
+ it("plans base files for partial installs and package files for complete ones", () => {
204
+ const { manifest } = kit();
205
+ const partial = resolveRegistryInstallSources({ manifest, requestedComponents: ["Text"] });
206
+ expect(partial.complete).toBe(false);
207
+ expect(partial.paths).toEqual([
208
+ "src/components/Text/index.tsx",
209
+ "src/styles/globals.scss",
210
+ "src/tokens/_variables.scss",
211
+ ]);
212
+
213
+ const complete = resolveRegistryInstallSources({
214
+ manifest,
215
+ requestedComponents: [],
216
+ all: true,
217
+ });
218
+ expect(complete.complete).toBe(true);
219
+ expect(complete.paths).toContain("src/index.ts");
220
+ expect(complete.paths).not.toContain("src/components/Button/Button.test.tsx");
221
+
222
+ const withTests = resolveRegistryInstallSources({
223
+ manifest,
224
+ requestedComponents: ["Button"],
225
+ includeTests: true,
226
+ });
227
+ expect(withTests.paths).toContain("src/components/Button/Button.test.tsx");
228
+ });
229
+
230
+ it("builds a plan whose dependencies follow the selection", () => {
231
+ const artifact = kit();
232
+ const plan = buildRegistryInstallPlan({
233
+ artifact,
234
+ requestedComponents: ["button"],
235
+ targetPath: "src/fragments/ui",
236
+ });
237
+ expect(plan.requestedComponents).toEqual(["Button"]);
238
+ expect(plan.impliedComponents).toEqual(["Text"]);
239
+ expect(plan.plans.map((entry) => entry.path)).toEqual([
240
+ "src/fragments/ui/components/Button/Button.module.scss",
241
+ "src/fragments/ui/components/Button/index.tsx",
242
+ "src/fragments/ui/components/Text/index.tsx",
243
+ "src/fragments/ui/index.ts",
244
+ "src/fragments/ui/styles/globals.scss",
245
+ "src/fragments/ui/tokens/_variables.scss",
246
+ "src/fragments/ui/utils/label.ts",
247
+ ]);
248
+ // Partial installs carry only the selected components' dependencies …
249
+ expect(plan.dependencies.map((dependency) => dependency.name)).toEqual([
250
+ "dep-Button",
251
+ "dep-Text",
252
+ ]);
253
+ // … complete installs add the package-level list.
254
+ const all = buildRegistryInstallPlan({
255
+ artifact,
256
+ requestedComponents: [],
257
+ all: true,
258
+ targetPath: "src/fragments/ui",
259
+ });
260
+ expect(all.dependencies.map((dependency) => dependency.name)).toContain("shared-dep");
261
+ expect(all.plans.map((entry) => entry.path)).toContain("src/fragments/ui/index.ts");
262
+ });
263
+ });