Setting the file. One moment.
Component Registry · Wix Headless Replatform · wix/skills · Skills Docs
ContentsBack to the top of the page 28.10
Workflow
scripts/lib/ component-registry.mjs
JavaScript · 248 lines · 12 KB
([
"astro-native"
,
"react-static"
,
"react-island"
]);
8
9 export async function loadApprovedRegistry ( registryRoot = new URL ( "../../registry/" , import . meta .url)) {
10 const root = registryRoot instanceof URL ? registryRoot : path. resolve (registryRoot);
11 const rootPath = root instanceof URL ? fileURLPath (root) : root;
12 const read = async ( relativePath ) => JSON . parse ( await readFile (root instanceof URL ? new URL (relativePath, root) : path. join (root, relativePath), "utf8" ));
13 const registry = await read ( "registry.json" );
14 const approvalsDir = root instanceof URL ? new URL ( "approvals/" , root) : path. join (root, "approvals" );
15 const approvals = new Map ();
16 for ( const file of await jsonFilesRecursive (approvalsDir)) {
17 const approval = JSON . parse ( await readFile (root instanceof URL ? new URL ( `approvals/${ file }` , root) : path. join (approvalsDir, file), "utf8" ));
18 if (approval.decision === "approved" && approval.revoked !== true ) {
19 approvals. set ( `${ approval . registryItemName }@${ approval . registryItemRevision }` , approval);
20 }
21 }
22 const items = [];
23 const rejected = [];
24 for ( const item of registry.items || []) {
25 if ( ! item.revision || ! item.sourceHash) {
26 rejected. push ({ item: item.name, reason: "registry-entry-is-not-immutable" });
27 continue ;
28 }
29 const approval = approvals. get ( `${ item . name }@${ item . revision }` );
30 if ( ! approval) {
31 rejected. push ({ item: item.name, reason: "missing-human-approval" });
32 continue ;
33 }
34 if ( ! REGISTRY_RUNTIME_CLASSES . has (approval.runtimeClass)) {
35 rejected. push ({ item: item.name, reason: "invalid-runtime-class" });
36 continue ;
37 }
38 if ( ! approval.reviewer?.human || ! approval.reviewer?.id) {
39 rejected. push ({ item: item.name, reason: "approval-is-not-attributed-to-a-human" });
40 continue ;
41 }
42 if ( ! approval.licenseRef || ! approval.sourceHash) {
43 rejected. push ({ item: item.name, reason: "approval-evidence-incomplete" });
44 continue ;
45 }
46 if (approval.sourceHash !== item.sourceHash) {
47 rejected. push ({ item: item.name, reason: "approval-source-hash-mismatch" });
48 continue ;
49 }
50 if ( ! item.capabilitiesRef || ! item.capabilitiesHash || ! item.contractRef || ! item.contractHash) {
51 rejected. push ({ item: item.name, reason: "runtime-contract-metadata-missing" });
52 continue ;
53 }
54 if (approval.capabilitiesRef !== item.capabilitiesRef || approval.capabilitiesHash !== item.capabilitiesHash
55 || approval.contractRef !== item.contractRef || approval.contractHash !== item.contractHash) {
56 rejected. push ({ item: item.name, reason: "approval-runtime-contract-mismatch" });
57 continue ;
58 }
59 try {
60 const capabilityBytes = await readFile ( safeJoin (rootPath, item.capabilitiesRef, "capability manifest" ));
61 const capabilityHash = createHash ( "sha256" ). update (capabilityBytes). digest ( "hex" );
62 if (capabilityHash !== item.capabilitiesHash) {
63 rejected. push ({ item: item.name, reason: "capability-manifest-hash-mismatch" });
64 continue ;
65 }
66 const capabilities = JSON . parse (capabilityBytes);
67 const validation = validateComponentCapabilities (capabilities, {
68 name: item.name,
69 revision: item.revision,
70 contract: approval.contractKind,
71 });
72 if ( ! validation.ok) {
73 rejected. push ({ item: item.name, reason: "capability-manifest-invalid" , details: validation.errors });
74 continue ;
75 }
76 const contractBytes = await readFile ( safeJoin (rootPath, item.contractRef, "agent contract" ));
77 const contractHash = createHash ( "sha256" ). update (contractBytes). digest ( "hex" );
78 if (contractHash !== item.contractHash) {
79 rejected. push ({ item: item.name, reason: "agent-contract-hash-mismatch" });
80 continue ;
81 }
82 if ( ! contractBytes. toString ( "utf8" ). trim ()) {
83 rejected. push ({ item: item.name, reason: "agent-contract-empty" });
84 continue ;
85 }
86 items. push ({ ... item, approval, capabilities });
87 } catch (error) {
88 rejected. push ({ item: item.name, reason: "capability-manifest-unreadable" , details: [error.message] });
89 }
90 }
91 return { registry: { ... registry, items }, rejected };
92 }
93
94 export function selectRegistryItem ( contract , approvedRegistry ) {
95 const hardRequirements = new Set (contract.hardRequirements || []);
96 const rejected = [ ... (approvedRegistry.rejected || [])];
97 const candidates = [];
98 for ( const item of approvedRegistry.registry?.items || []) {
99 const approval = item.approval;
100 const reasons = [];
101 if (approval.contractKind !== contract.kind) reasons. push ( "contract-kind-mismatch" );
102 if (contract.requiresClientRuntime && approval.runtimeClass === "react-static" ) reasons. push ( "interaction-requires-runtime-state" );
103 for ( const requirement of hardRequirements) if (approval.qualityGates?.[requirement] !== true ) reasons. push ( `quality-gate:${ requirement }` );
104 const capability = resolveCapabilityBinding (item.capabilities, contract.capabilityRequirements || {});
105 reasons. push ( ... capability.reasons);
106 if (reasons. length ) rejected. push ({ item: `${ item . name }@${ item . revision }` , reason: reasons. join ( "," ) });
107 else candidates. push ({ item, binding: capability.binding });
108 }
109 candidates. sort (( a , b ) => {
110 const aDeps = Object. keys (a.item.dependencies || {}). length + (a.item.registryDependencies || []). length ;
111 const bDeps = Object. keys (b.item.dependencies || {}). length + (b.item.registryDependencies || []). length ;
112 return aDeps - bDeps || a.item.name. localeCompare (b.item.name);
113 });
114 if ( ! candidates. length ) return { strategy: "bounded-custom" , selected: null , rejected };
115 const winner = candidates[ 0 ];
116 return {
117 strategy: "curated-registry" ,
118 selected: {
119 name: winner.item.name,
120 type: winner.item.type,
121 files: winner.item.files,
122 runtimeClass: winner.item.approval.runtimeClass,
123 sourceHash: winner.item.approval.sourceHash,
124 approvalRef: winner.item.approval.approvalRef,
125 revision: winner.item.revision,
126 dependencies: winner.item.dependencies || {},
127 capabilitiesRef: winner.item.capabilitiesRef,
128 capabilitiesHash: winner.item.capabilitiesHash,
129 contractRef: winner.item.contractRef,
130 contractHash: winner.item.contractHash,
131 binding: winner.binding,
132 },
133 rejected: [ ... rejected, ... candidates. slice ( 1 ). map (({ item }) => ({ item: `${ item . name }@${ item . revision }` , reason: "higher-adaptation-or-dependency-cost" }))],
134 };
135 }
136
137 export async function installRegistrySelection ({ selection , outputDir , registryRoot = new URL ( "../../registry/" , import . meta .url) }) {
138 if ( ! selection || selection.strategy !== "curated-registry" || ! selection.selected) return { strategy: "bounded-custom" , installed: [] };
139 const root = registryRoot instanceof URL ? fileURLPath (registryRoot) : path. resolve (registryRoot);
140 const destinationRoot = path. resolve (outputDir);
141 const installed = [];
142 for ( const file of selection.selected.files || []) {
143 if ( ! file.path || ! file.target || ! file.sha256) throw new Error ( `Registry file metadata is incomplete for ${ selection . selected . name }` );
144 const source = safeJoin (root, file.path, "registry source" );
145 const destination = safeJoin (destinationRoot, file.target, "registry target" );
146 const sourceBytes = await readFile (source);
147 const actualHash = createHash ( "sha256" ). update (sourceBytes). digest ( "hex" );
148 if (actualHash !== file.sha256) throw new Error ( `Registry source hash mismatch for ${ file . path }: expected ${ file . sha256 }, got ${ actualHash }` );
149 await mkdir (path. dirname (destination), { recursive: true });
150 await copyFile (source, destination);
151 installed. push ({ source: file.path, target: file.target, sha256: actualHash });
152 }
153 const dependencies = selection.selected.dependencies || {};
154 if (Array. isArray (dependencies)) throw new Error ( `Registry dependencies must be exact name/version pairs for ${ selection . selected . name }` );
155 for ( const [ name , version ] of Object. entries (dependencies)) {
156 if ( ! / ^ \d + \. \d + \. \d + (?: [-+][0-9A-Za-z.-] + ) ?$ / . test ( String (version))) throw new Error ( `Registry dependency ${ name } must use an exact version, got ${ version }` );
157 }
158 if (Object. keys (dependencies). length ) {
159 const packagePath = path. join (destinationRoot, "package.json" );
160 const packageJson = JSON . parse ( await readFile (packagePath, "utf8" ));
161 packageJson.dependencies = { ... (packageJson.dependencies || {}), ... dependencies };
162 packageJson.dependencies = Object. fromEntries (Object. entries (packageJson.dependencies). sort (([ a ], [ b ]) => a. localeCompare (b)));
163 await writeFile (packagePath, `${ JSON . stringify ( packageJson , null , 2 ) } \n ` );
164 }
165 const astroConfiguration = selection.selected.runtimeClass?. startsWith ( "react-" )
166 ? await ensureAstroReactIntegration (destinationRoot)
167 : { changed: false , reason: "not-required" };
168 return {
169 strategy: "curated-registry" ,
170 item: selection.selected.name,
171 revision: selection.selected.revision,
172 sourceHash: selection.selected.sourceHash,
173 capabilitiesRef: selection.selected.capabilitiesRef,
174 capabilitiesHash: selection.selected.capabilitiesHash,
175 contractRef: selection.selected.contractRef,
176 contractHash: selection.selected.contractHash,
177 binding: selection.selected.binding,
178 approvalRef: selection.selected.approvalRef,
179 installed,
180 dependencies,
181 astroConfiguration,
182 };
183 }
184
185 async function ensureAstroReactIntegration ( destinationRoot ) {
186 const configPath = path. join (destinationRoot, "astro.config.mjs" );
187 let source;
188 try { source = await readFile (configPath, "utf8" ); }
189 catch (error) {
190 if (error?.code === "ENOENT" ) throw new Error ( "React registry installation requires an existing astro.config.mjs" );
191 throw error;
192 }
193 const reactImport = source. match ( /import \s + ( [A-Za-z_$][\w$] * ) \s + from \s + ["'] @astrojs \/ react ["'] / );
194 const reactIdentifier = reactImport?.[ 1 ] || "react" ;
195 const hasImport = Boolean (reactImport);
196 const hasIntegration = new RegExp ( `integrations \\ s*: \\ s* \\ [[^ \\ ]]* \\ b${ reactIdentifier } \\ s* \\ (` , "s" ). test (source);
197 if (hasImport && hasIntegration) return { changed: false , config: "astro.config.mjs" };
198 if ( ! /defineConfig \s * \( \s * \{ / . test (source)) {
199 throw new Error ( "Cannot safely add the React integration: astro.config.mjs must export defineConfig({...})" );
200 }
201 let next = source;
202 if ( ! hasImport) next = `import react from "@astrojs/react"; \n ${ next }` ;
203 if ( ! hasIntegration) {
204 if ( /integrations \s * : \s * \[ / . test (next)) {
205 next = next. replace ( /integrations \s * : \s * \[ / , ( match ) => `${ match }${ reactIdentifier }(), ` );
206 } else {
207 next = next. replace ( /defineConfig \s * \( \s * \{ / , ( match ) => `${ match } \n integrations: [${ reactIdentifier }()],` );
208 }
209 }
210 await writeFile (configPath, next);
211 return {
212 changed: true ,
213 config: "astro.config.mjs" ,
214 sha256: createHash ( "sha256" ). update (next). digest ( "hex" ),
215 };
216 }
217
218 async function jsonFilesRecursive ( dir , prefix = "" ) {
219 try {
220 const entries = await readdir (dir instanceof URL ? dir : path. resolve (dir), { withFileTypes: true });
221 const files = [];
222 for ( const entry of entries) {
223 const relative = prefix ? `${ prefix }/${ entry . name }` : entry.name;
224 if (entry. isFile () && entry.name. endsWith ( ".json" )) files. push (relative);
225 if (entry. isDirectory ()) {
226 const child = dir instanceof URL ? new URL ( `${ entry . name }/` , dir) : path. join (dir, entry.name);
227 files. push ( ...await jsonFilesRecursive (child, relative));
228 }
229 }
230 return files. sort ();
231 } catch (error) {
232 if (error?.code === "ENOENT" ) return [];
233 throw error;
234 }
235 }
236
237 function safeJoin ( root , relativePath , label ) {
238 if (path. isAbsolute (relativePath) || relativePath. split ( / [ \\ /] + / ). includes ( ".." )) throw new Error ( `${ label } must be a safe relative path: ${ relativePath }` );
239 const normalizedRoot = path. resolve (root);
240 const target = path. resolve (normalizedRoot, relativePath);
241 if (target !== normalizedRoot && ! target. startsWith ( `${ normalizedRoot }${ path . sep }` )) throw new Error ( `${ label } escapes its root: ${ relativePath }` );
242 return target;
243 }
244
245 function fileURLPath ( value ) {
246 if (value.protocol !== "file:" ) throw new Error ( `Registry URL must use file: ${ value }` );
247 return fileURLToPath (value);
248 }