@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.
- package/dist/{chunk-MZ4SW3TP.js → chunk-3IOWHECM.js} +8 -1
- package/dist/{chunk-MZ4SW3TP.js.map → chunk-3IOWHECM.js.map} +1 -1
- package/dist/chunk-7ULGH74M.js +66 -0
- package/dist/chunk-7ULGH74M.js.map +1 -0
- package/dist/{chunk-XN3LSDPY.js → chunk-BMPYIUZE.js} +612 -344
- package/dist/chunk-BMPYIUZE.js.map +1 -0
- package/dist/{chunk-YF65VYRY.js → chunk-ML5S6QNU.js} +191 -169
- package/dist/chunk-ML5S6QNU.js.map +1 -0
- package/dist/{chunk-3LLRNCPX.js → chunk-MZ2FS7U4.js} +1 -1
- package/dist/chunk-MZ2FS7U4.js.map +1 -0
- package/dist/chunk-PWIJMOI4.js +202 -0
- package/dist/chunk-PWIJMOI4.js.map +1 -0
- package/dist/chunk-RYFULE43.js +578 -0
- package/dist/chunk-RYFULE43.js.map +1 -0
- package/dist/codes/index.d.ts +2 -2
- package/dist/codes/index.js +3 -2
- package/dist/compiled-types/index.d.ts +327 -2
- package/dist/compiled-types/index.js +1 -1
- package/dist/generate/index.d.ts +3 -2
- package/dist/{governance-CLk_wkP9.d.ts → governance-hOPXGbbs.d.ts} +474 -515
- package/dist/governance-telemetry.d.ts +6 -0
- package/dist/governance-telemetry.js +1 -1
- package/dist/{index-_sxhUNqx.d.ts → index-C8bcXVav.d.ts} +448 -448
- package/dist/index.d.ts +3280 -1305
- package/dist/index.js +2414 -350
- package/dist/index.js.map +1 -1
- package/dist/manifest.d.ts +228 -0
- package/dist/manifest.js +24 -0
- package/dist/manifest.js.map +1 -0
- package/dist/preview/index.js +45 -1
- package/dist/preview/index.js.map +1 -1
- package/dist/preview-runtime.d.ts +1 -2
- package/dist/preview-runtime.js +150 -14
- package/dist/preview-runtime.js.map +1 -1
- package/dist/react-types.d.ts +1 -2
- package/dist/registry.d.ts +1412 -203
- package/dist/registry.js +30 -3
- package/dist/schemas/index.d.ts +1 -1
- package/dist/schemas/index.js +3 -2
- package/dist/storyAdapter.d.ts +1 -2
- package/dist/storyAdapter.js +11 -49
- package/dist/storyAdapter.js.map +1 -1
- package/dist/test-utils.d.ts +3 -2
- package/dist/topology/index.d.ts +1 -1
- package/dist/topology/index.js +1 -1
- package/package.json +8 -2
- package/src/__tests__/contract-parser.test.ts +318 -277
- package/src/__tests__/preview-runtime-hook.test.tsx +315 -0
- package/src/__tests__/preview-runtime.test.tsx +30 -8
- package/src/__tests__/schema.test.ts +191 -14
- package/src/analysis-plan/analysis-plan-v1.test.ts +320 -0
- package/src/analysis-plan/coverage.ts +181 -0
- package/src/analysis-plan/digest.ts +141 -0
- package/src/analysis-plan/index.ts +34 -0
- package/src/analysis-plan/types.ts +207 -0
- package/src/approved-contract-tokens.test.ts +39 -0
- package/src/approved-contract-tokens.ts +18 -0
- package/src/codes/__tests__/codes.test.ts +13 -0
- package/src/codes/codes.ts +40 -0
- package/src/compiled-types/index.ts +640 -39
- package/src/compiled-types/parse.test.ts +145 -4
- package/src/component-contract.ts +95 -53
- package/src/composition.ts +7 -13
- package/src/constants.ts +3 -6
- package/src/contract/hash.test.ts +20 -0
- package/src/contract/hash.ts +66 -9
- package/src/contract/index.ts +24 -1
- package/src/contract/manifest.test.ts +94 -0
- package/src/contract/manifest.ts +68 -0
- package/src/contract/preimage.test.ts +219 -1
- package/src/contract/preimage.ts +326 -6
- package/src/contract/stamp.test.ts +3 -0
- package/src/contract/stamp.ts +1 -1
- package/src/contract-parser.ts +54 -30
- package/src/defineFragment.test.ts +476 -91
- package/src/defineFragment.ts +204 -114
- package/src/domain-ids.test.ts +35 -0
- package/src/domain-ids.ts +61 -0
- package/src/evaluation/evaluate.test.ts +522 -0
- package/src/evaluation/evaluate.ts +690 -0
- package/src/evaluation/evaluation-v2-receipt-v1.test.ts +772 -0
- package/src/evaluation/index.ts +58 -0
- package/src/evaluation/receipt.ts +753 -0
- package/src/evaluation/types.ts +406 -0
- package/src/facts/builders.ts +2 -0
- package/src/facts/compile.ts +29 -6
- package/src/facts/fact-index.ts +13 -3
- package/src/facts/fact-integrity-v1.test.ts +172 -0
- package/src/facts/facts.test.ts +15 -0
- package/src/facts/ids.ts +46 -3
- package/src/facts/index.ts +14 -1
- package/src/facts/integrity.ts +134 -0
- package/src/facts/types.ts +36 -0
- package/src/governance-integrity.test.ts +1 -0
- package/src/governance-integrity.ts +5 -3
- package/src/governance-telemetry.ts +8 -0
- package/src/governance.ts +70 -8
- package/src/index.ts +230 -37
- package/src/preview/validation.test.ts +62 -0
- package/src/preview/validation.ts +48 -2
- package/src/preview-runtime.tsx +227 -20
- package/src/registry-install-plan.ts +200 -109
- package/src/registry-shards.test.ts +263 -0
- package/src/registry.ts +237 -0
- package/src/repository-binding.test.ts +50 -0
- package/src/repository-binding.ts +96 -0
- package/src/rules/families.test.ts +36 -0
- package/src/rules/finding.ts +7 -2
- package/src/rules/index.ts +17 -1
- package/src/rules/rule-config.test.ts +66 -0
- package/src/rules/rule-config.ts +73 -0
- package/src/rules/rules.test.ts +26 -0
- package/src/rules/tokens-css-vars-must-be-defined.test.ts +51 -2
- package/src/rules/tokens-css-vars-must-be-defined.ts +34 -1
- package/src/schema.ts +293 -113
- package/src/schemas/index.ts +1 -1
- package/src/storyAdapter.test.ts +68 -12
- package/src/storyAdapter.ts +44 -75
- package/src/topology/resolve-area.ts +1 -1
- package/src/types.ts +258 -40
- package/dist/chunk-3LLRNCPX.js.map +0 -1
- package/dist/chunk-RANPUC6C.js +0 -72
- package/dist/chunk-RANPUC6C.js.map +0 -1
- package/dist/chunk-XN3LSDPY.js.map +0 -1
- package/dist/chunk-YF65VYRY.js.map +0 -1
- package/src/fragment-types.ts +0 -214
- package/src/react-create-element.test.ts +0 -22
- package/src/react-create-element.ts +0 -12
- package/src/storyFilters.test.ts +0 -350
- 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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
): string
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
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
|
-
|
|
71
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
|
|
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
|
-
|
|
160
|
+
manifest: RegistryManifest,
|
|
83
161
|
componentNames: readonly string[]
|
|
84
162
|
): boolean {
|
|
85
|
-
const allComponents = Object.keys(
|
|
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
|
|
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
|
-
|
|
107
|
-
|
|
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
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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
|
-
|
|
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
|
|
195
|
-
artifact
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
314
|
+
manifest: RegistryManifest;
|
|
240
315
|
}): RegistryWritePlan {
|
|
241
316
|
const lines = args.componentNames.map((name) => {
|
|
242
|
-
const component = args.
|
|
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
|
|
273
|
-
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
...
|
|
284
|
-
|
|
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({
|
|
383
|
+
: [generatedIndexPlan({ manifest, componentNames, targetPath })]),
|
|
299
384
|
]);
|
|
300
385
|
|
|
301
|
-
return {
|
|
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
|
+
});
|