Setting the file. One moment.
SDK Lookup · Wix App · wix/skills · Skills Docs
ContentsBack to the top of the page — line 168
This file
Number 1.204
Position 204 of 205
Type JavaScript
Size 59 KB
Lines 1,225 scripts/ sdk-lookup.cjs
JavaScript · 1,225 lines · 59 KB
15 // generated package carries.
16 //
17 // Exit codes: 0 every entity found · 1 an entity matched nothing (near matches
18 // printed) · 3 an entity's package is not installed (install command printed)
19 // · 2 usage · 4 an installed package is in a form the lookup does not read.
20 const fs = require ( 'fs' );
21 const path = require ( 'path' );
22
23 class LookupError extends Error {}
24
25 const INTENTS = {
26 search: [ 'search' , 'query-builder' , 'query' , 'list' ],
27 read: [ 'query-builder' , 'query' , 'search' , 'list' , 'get' , 'count' ],
28 write: [ 'create' , 'update' , 'upsert' , 'delete' , 'bulk' ],
29 event: [],
30 all: null ,
31 };
32 // `all` lists every method, the search ones first, in this order.
33 const ALL_ORDER = [ ... INTENTS .search, 'get' , 'count' , ... INTENTS .write];
34 const searching = ( intent ) => intent === 'search' || intent === 'all' ;
35
36 // --- package resolution ------------------------------------------------------
37
38 function activatePnp ( from ) {
39 for ( let dir = from; ; dir = path. dirname (dir)) {
40 const pnp = path. join (dir, '.pnp.cjs' );
41 if (fs. existsSync (pnp)) {
42 try {
43 require (pnp). setup ();
44 } catch {
45 // Not fatal: node_modules resolution below may still work.
46 }
47 return ;
48 }
49 if (path. dirname (dir) === dir) return ;
50 }
51 }
52
53 function resolveDir ( name , from ) {
54 try {
55 return path. dirname (require. resolve ( `${ name }/package.json` , { paths: [from] }));
56 } catch {
57 for ( let dir = from; ; dir = path. dirname (dir)) {
58 const candidate = path. join (dir, 'node_modules' , ... name. split ( '/' ));
59 if (fs. existsSync (path. join (candidate, 'package.json' ))) return candidate;
60 if (path. dirname (dir) === dir) return null ;
61 }
62 }
63 }
64
65 const readJson = ( file ) => JSON . parse (fs. readFileSync (file, 'utf8' ));
66 // contacts-v-5 → contactsV5, order-transactions → orderTransactions
67 const camel = ( s ) => s. replace ( /-v-( \d + ) $ / , 'V$1' ). replace ( /-( \w )/ g , ( _ , c ) => c. toUpperCase ());
68 const isAuto = ( name ) => name. startsWith ( '@wix/auto_sdk_' );
69
70 function wixDirsAbove ( from ) {
71 const out = [];
72 for ( let dir = from; ; dir = path. dirname (dir)) {
73 const wix = path. join (dir, 'node_modules' , '@wix' );
74 if (fs. existsSync (wix)) out. push (wix);
75 if (path. dirname (dir) === dir) return out;
76 }
77 }
78
79 // Finds every public SDK package (one that depends on @wix/auto_sdk_*
80 // packages) and every auto package, with the namespace each is exported as.
81 function findPackages ( project ) {
82 const wrappers = new Map (); // name -> dir
83 const consider = ( name , from ) => {
84 if (wrappers. has (name) || isAuto (name) || ! name. startsWith ( '@wix/' )) return ;
85 const dir = resolveDir (name, from);
86 if ( ! dir) return ;
87 const deps = Object. keys ( readJson (path. join (dir, 'package.json' )).dependencies ?? {});
88 if (deps. some (isAuto)) wrappers. set (name, dir);
89 };
90 const pj = path. join (project, 'package.json' );
91 if (fs. existsSync (pj)) {
92 const { dependencies = {}, devDependencies = {} } = readJson (pj);
93 for ( const name of Object. keys ({ ... dependencies, ... devDependencies })) consider (name, project);
94 }
95 for ( const wix of wixDirsAbove (project)) {
96 for ( const d of fs. readdirSync (wix)) consider ( `@wix/${ d }` , project);
97 }
98
99 const autos = new Map (); // name -> { dir, exportedAs: [] }
100 for ( const [ wrapper , dir ] of wrappers) {
101 const deps = Object. keys ( readJson (path. join (dir, 'package.json' )).dependencies ?? {});
102 const barrel = [ 'build/cjs/index.d.ts' , 'build/es/index.d.mts' ]
103 . map (( f ) => path. join (dir, f))
104 . find (( f ) => fs. existsSync (f));
105 const src = barrel ? fs. readFileSync (barrel, 'utf8' ) : '' ;
106 for ( const dep of deps. filter (isAuto)) {
107 const depDir = resolveDir (dep, dir);
108 if ( ! depDir) continue ;
109 const entry = autos. get (dep) ?? { dir: depDir, exportedAs: [] };
110 const m = src. match ( new RegExp ( `import \\ * as ( \\ w+) from '${ dep }'; \\ s*export \\ { \\ 1 as ( \\ w+) \\ }` ));
111 if (m) entry.exportedAs. push ({ from: wrapper, namespace: m[ 2 ] });
112 // The export file names the package in a form this regex does not read
113 // (`export * as ns from '…'`): stop, or it is labeled "not exported". A bare
114 // `import '…'` (app-management's custom-charges) exports nothing.
115 else if ( new RegExp ( `(export|import \\ * as \\ w+)[^;']*from '${ dep }'` ). test (src)) {
116 throw new LookupError ( `${ wrapper } exports ${ dep } in a form sdk-lookup does not read (${ path . relative ( dir , barrel ) }); its namespace is unknown, so do not trust this lookup` );
117 }
118 autos. set (dep, entry);
119 }
120 }
121 // An auto package on disk that no installed wrapper re-exports is either a
122 // dependency of another package (its own wrapper is not installed: name it
123 // and the install command) or an SPI/internal package of an installed
124 // wrapper (not importable from the wrapper's root).
125 for ( const wix of wixDirsAbove (project)) {
126 for ( const d of fs. readdirSync (wix). filter (( d ) => d. startsWith ( 'auto_sdk_' ))) {
127 const name = `@wix/${ d }` ;
128 if ( ! autos. has (name)) autos. set (name, { dir: path. join (wix, d), exportedAs: [] });
129 }
130 }
131 for ( const [ name , entry ] of autos) {
132 if (entry.exportedAs. length ) continue ;
133 const [, vertical , ns ] = name. match ( / ^ @wix \/ auto_sdk_( [ ^ _] + )_( . + ) $ / ) ?? [];
134 const wrapper = vertical ? `@wix/${ vertical }` : null ;
135 if (wrapper && ! wrappers. has (wrapper)) {
136 entry.exportedAs. push ({ from: wrapper, namespace: camel (ns), installed: false });
137 }
138 }
139 return { wrappers, autos };
140 }
141
142 // --- deriving a record from .d.ts --------------------------------------------
143
144 // Reads one `declare function name(...): Ret;` starting at `at`, balancing
145 // brackets so callback parameters and generic return types stay whole.
146 function readDeclaration ( src , at ) {
147 let depth = 0 ;
148 let i = src. indexOf ( '(' , at);
149 const paramsStart = i + 1 ;
150 let paramsEnd = - 1 ;
151 for (; i < src. length ; i ++ ) {
152 const c = src[i];
153 if (c === '(' || c === '<' || c === '{' || c === '[' ) depth ++ ;
154 else if (c === ')' || c === '>' || c === '}' || c === ']' ) {
155 if (src[i - 1 ] === '=' && c === '>' ) continue ; // arrow
156 depth -- ;
157 if (depth === 0 && paramsEnd < 0 && c === ')' ) {
158 paramsEnd = i;
159 if (src[i + 1 ] !== ':' ) break ;
160 }
161 } else if (c === ';' && depth === 0 && paramsEnd >= 0 ) break ;
162 }
163 const params = src. slice (paramsStart, paramsEnd);
164 const ret = src. slice (paramsEnd + 1 , i). replace ( / ^ : \s * / , '' ). trim ();
165 return { params, ret, end: i };
166 }
167
168 const tags = ( doc , tag ) =>
169 [ ... doc. matchAll ( new RegExp ( `@${ tag }(?: \\ s+([^ \\ n*]*))?` , 'g' ))]. map (( m ) => (m[ 1 ] ?? '' ). trim ());
170
171 // The first sentence of the JSDoc right before `at`: what the entity is ("An
172 // order represents a completed purchase from a buyer."), which tells apart
173 // the packages that export the same namespace. Null when there is none.
174 // One JSDoc block as one line of text, links removed.
175 const jsdocLine = ( doc ) =>
176 doc. split ( ' \n ' ). map (( l ) => l. replace ( / ^ \s * \* \s ? / , '' ). trim ()). join ( ' ' ). replace ( / \[ ( [ ^ \] ] + ) \]\( [ ^ )] * \) / g , '$1' ). replace ( / \s + / g , ' ' ). trim ();
177
178 function descriptionBefore ( src , at ) {
179 const end = src. lastIndexOf ( '*/' , at);
180 if (end < 0 || src. slice (end + 2 , at). trim ()) return null ;
181 const text = src
182 . slice (src. lastIndexOf ( '/**' , end) + 3 , end)
183 . split ( ' \n ' )
184 . map (( l ) => l. replace ( / ^ \s * \* \s ? / , '' ). trim ())
185 . join ( ' ' )
186 . replace ( / \[ ( [ ^ \] ] + ) \]\( [ ^ )] * \) / g , '$1' )
187 . replace ( / \s + / g , ' ' )
188 . trim ();
189 if ( ! text || text. startsWith ( '@' )) return null ;
190 return text. match ( / ^ . *? [.!?] (?= \s |$ )/ )?.[ 0 ] ?? text. split ( ' @' )[ 0 ];
191 }
192
193 // The `> **Note:**` blocks of a method's JSDoc, the same text as its docs page.
194 // They carry who may call it ("requires visitor or member authentication"),
195 // which @applicableIdentity does not: getCurrentMember is tagged APP alone.
196 const UPDATE_RULE = / \b to (clear | remove) (a | the) \b [ ^ .] *\b (value | field) |\b (replaces | overwrites) the (whole | entire) array \b|\b partial update \b|\b only the (requested | provided | specified | listed) fields \b / i ;
197
198 function notesOf ( doc ) {
199 const lines = doc. split ( ' \n ' ). map (( l ) => l. replace ( / ^ \s * \* \s ? / , '' ));
200 const notes = [];
201 for ( let i = 0 ; i < lines. length ; i ++ ) {
202 const head = / ^ > \s * \*\* Notes ? : \*\* / . exec (lines[i]);
203 if ( ! head) continue ;
204 const text = [lines[i]. slice (head[ 0 ]. length )];
205 while (i + 1 < lines. length && lines[i + 1 ]. startsWith ( '>' )) text. push (lines[ ++ i]. slice ( 1 ));
206 const note = text. join ( ' ' ). replace ( / \[ ( [ ^ \] ] + ) \]\( [ ^ )] * \) / g , '$1' ). replace ( / \s + / g , ' ' ). trim ();
207 if (note) notes. push (note);
208 }
209 // An update's field rules are plain description text, not a Note block:
210 // without "To clear a field's value, set … `""`" an agent sends undefined,
211 // which the PATCH drops, and the old value stays.
212 const tagAt = lines. findIndex (( l ) => l. startsWith ( '@' ));
213 const desc = lines. slice ( 0 , tagAt < 0 ? lines. length : tagAt). filter (( l ) => ! l. startsWith ( '>' )). join ( ' ' );
214 for ( const s of desc. split ( /(?<= \. ) \s + (?= [A-Z`] )/ )) {
215 if ( ! UPDATE_RULE . test (s)) continue ;
216 const rule = s. replace ( / \[ ( [ ^ \] ] + ) \]\( [ ^ )] * \) / g , '$1' ). replace ( / \s + / g , ' ' ). trim ();
217 if ( ! notes. includes (rule)) notes. push (rule);
218 }
219 return notes;
220 }
221
222 function kindOf ( name , params , ret ) {
223 if ( / ^ on [A-Z] / . test (name)) return 'event' ;
224 if ( / ^ search/ . test (name)) return 'search' ;
225 if ( / ^ query/ . test (name)) {
226 if ( /QueryBuilder/ . test (ret)) return 'query-builder' ;
227 return / ^ \s * (query | options) \? ? :/ . test (params) || /Query \b / . test (params) ? 'query' : 'other' ;
228 }
229 for ( const k of [ 'list' , 'get' , 'count' , 'create' , 'update' , 'upsert' , 'bulk' ]) {
230 if (name. startsWith (k)) return k;
231 }
232 if ( / ^ (delete | remove)/ . test (name)) return 'delete' ;
233 return 'other' ;
234 }
235
236 // Drops the noise the generator puts in signatures, so a line stays readable.
237 function shorten ( type , max = 90 ) {
238 let t = type
239 . replace ( /NonNullablePaths< \s * ( [\w.] + ) \s * , [ ^ >] *? , \s * \d + \s * >/ g , '$1' )
240 . replace ( / \s + & \s + \{ [\s\S] *$ / , '' )
241 . replace ( / \s + / g , ' ' )
242 . trim ();
243 const open = (t. match ( /</ g ) ?? []). length - (t. match ( />/ g ) ?? []). length ;
244 if (open > 0 ) t += '>' . repeat (open);
245 if (t. length > max) t = `${ t . slice ( 0 , max - 1 ) }…` ;
246 return t;
247 }
248
249 function requiredOf ( params ) {
250 const out = new Set ();
251 for ( const m of params. matchAll ( /NonNullablePaths< \s * [\w.] + \s * ,( [ ^ >] *? ), \s * \d + \s * >/ g )) {
252 for ( const f of m[ 1 ]. matchAll ( /`( [ ^ `] + )`/ g )) out. add (f[ 1 ]);
253 }
254 for ( const m of params. matchAll ( /(?: ^| ,) \s * (revision) \s * :/ g )) out. add (m[ 1 ]);
255 return [ ... out];
256 }
257
258 function paramList ( params ) {
259 return splitParams (params). map (( p ) => {
260 const m = p. match ( / ^ ( \w + \? ? ) \s * : \s * ( [\s\S] * ) $ / );
261 return m ? `${ m [ 1 ] }: ${ shorten ( m [ 2 ], 40 ) }` : shorten (p, 40 );
262 });
263 }
264
265 // Splits a parameter list on its top-level commas.
266 function splitParams ( params ) {
267 const out = [];
268 let depth = 0 ;
269 let cur = '' ;
270 for ( const c of params) {
271 if ( '(<{[' . includes (c)) depth ++ ;
272 if ( ')>}]' . includes (c)) depth -- ;
273 if (c === ',' && depth === 0 ) {
274 out. push (cur);
275 cur = '' ;
276 } else cur += c;
277 }
278 if (cur. trim ()) out. push (cur);
279 return out. map (( p ) => p. trim ()). filter (Boolean);
280 }
281
282 const pascal = ( s ) => s. replace ( /( ^| [_-] )( \w )/ g , ( _ , __ , c ) => c. toUpperCase ());
283
284 // The body of `interface Name {…}` or `type Name = {…}`, skipping an empty `{}`.
285 function typeBody ( sources , name ) {
286 const re = new RegExp ( `^(?:interface ${ name } \\ b[^{ \\ n]*|type ${ name } \\ s*= \\ s*) \\ {` , 'gm' );
287 for ( const src of sources) {
288 for ( const m of src. matchAll (re)) {
289 let depth = 0 ;
290 let i = m.index + m[ 0 ]. length - 1 ;
291 for (; i < src. length ; i ++ ) {
292 if (src[i] === '{' ) depth ++ ;
293 else if (src[i] === '}' && -- depth === 0 ) break ;
294 }
295 const body = src. slice (m.index + m[ 0 ]. length , i);
296 if (body. trim ()) return body;
297 }
298 }
299 return null ;
300 }
301
302 // Top-level members of a type body: [{ name, type }], comments dropped.
303 function membersOf ( body ) {
304 const clean = body. replace ( / \/\* [\s\S] *? \*\/ / g , '' ). replace ( / \$\{ [ ^ }] * \} / g , '' );
305 const out = [];
306 let depth = 0 ;
307 for ( const line of clean. split ( ' \n ' )) {
308 const m = depth === 0 && line. match ( / ^ \s * ( \w + ) \? ? \s * : \s * ( . * ) $ / );
309 if (m) out. push ({ name: m[ 1 ], type: m[ 2 ] });
310 else if (out. length && depth > 0 ) out[out. length - 1 ].type += ` \n ${ line }` ;
311 depth += (line. match ( / \{ / g ) ?? []). length - (line. match ( / \} / g ) ?? []). length ;
312 }
313 return out;
314 }
315
316 // `{ a, b: { c, d }, e: [{ f }] }`: the keys of a type, two levels deep.
317 // Named types are followed, except the entity type (its fields are in `type`).
318 function typeShape ( sources , name , skip , depth = 0 ) {
319 const body = typeBody (sources, name);
320 return body ? shapeOfBody (sources, body, skip, depth) : null ;
321 }
322
323 function shapeOfBody ( sources , body , skip , depth ) {
324 const keys = membersOf (body). map (({ name , type }) => {
325 if (depth >= 2 ) return name;
326 const t = type. trim ();
327 const list = / \[\] \s * ; ? \s *$ / . test (t) || / ^ Array</ . test (t);
328 let inner = null ;
329 if (t. startsWith ( '{' )) inner = shapeOfBody (sources, t. slice ( 1 , t. lastIndexOf ( '}' )), skip, depth + 1 );
330 else {
331 const ref = t. match ( / ^ (?:Array<) ? ( \w + )/ )?.[ 1 ];
332 if (ref && ! skip. has (ref) && / ^ [A-Z] / . test (ref)) inner = typeShape (sources, ref, skip, depth + 1 );
333 }
334 if ( ! inner) {
335 const ref = t. match ( / ^ ( \w + )( \[\] ) ? / );
336 return ref && skip. has (ref[ 1 ]) ? `${ name }: ${ ref [ 1 ] }${ ref [ 2 ] ?? ''}` : name;
337 }
338 return `${ name }: ${ list ? `[${ inner }]` : inner }` ;
339 });
340 if ( ! keys. length ) return null ;
341 return `{ ${ keys . length > 8 ? [ ... keys . slice ( 0 , 8 ), '…' ]. join ( ', ' ) : keys . join ( ', ' ) } }` ;
342 }
343
344 // The declared type of a dotted field path (`contact.firstName`) in a type
345 // body, with `| null` dropped; a builder's `category.id` is the type's `_id`.
346 function fieldType ( sources , body , dotted ) {
347 const parts = dotted. split ( '.' );
348 for ( let i = 0 ; body && i < parts. length ; i ++ ) {
349 const m = membersOf (body). find (( x ) => x.name === parts[i] || x.name === `_${ parts [ i ] }` );
350 if ( ! m) return null ;
351 const t = m.type. trim (). replace ( /; $ / , '' ). replace ( / \s * \| \s * (null | undefined) \b / g , '' ). trim ();
352 if (i === parts. length - 1 ) return t;
353 body = t. startsWith ( '{' ) ? t. slice ( 1 , t. lastIndexOf ( '}' )) : typeBody (sources, t. match ( / ^ ( \w + )/ )?.[ 1 ] ?? '' );
354 }
355 return null ;
356 }
357
358 function limitsOf ( sources , name , skip , prefix = '' , depth = 0 , out = [], seen = new Set ()) {
359 if (depth > 2 || seen. has (name)) return out;
360 seen. add (name);
361 const body = typeBody (sources, name) ?? '' ;
362 for ( const { name : key , type } of membersOf (body)) {
363 const t = type. trim (). replace ( /; $ / , '' ). replace ( / \s * \| \s * (null | undefined) \b / g , '' ). trim ();
364 const doc = body. match ( new RegExp ( `/ \\ * \\ *((?:(?! \\ */)[ \\ s \\ S])*?) \\ */ \\ s*${ key } \\ ?? \\ s*:` ))?.[ 1 ] ?? '' ;
365 // An enum array's @maxSize is almost always at least the enum's size, so it is skipped.
366 const caps = / \[\] $|^ Array</ . test (t) && ! /WithLiterals/ . test (t) ? [ 'minSize' , 'maxSize' ] : / ^ number \b / . test (t) ? [ 'min' , 'max' ] : [];
367 const limit = { path: prefix ? `${ prefix }.${ key }` : key };
368 for ( const tag of caps) {
369 const v = tags (doc, tag). find (( x ) => / ^ - ? \d + ( \. \d + ) ?$ / . test (x));
370 if (v != null ) limit[tag] = Number (v);
371 }
372 if (Object. keys (limit). length > 1 && (depth === 0 || /( ^| \. )(paging | cursorPaging) \. limit $ / . test (limit.path))) out. push (limit);
373 const ref = t. match ( / ^ (?:Array<) ? ( [A-Z]\w * )/ )?.[ 1 ];
374 if (ref && ! skip. has (ref)) limitsOf (sources, ref, skip, limit.path, depth + 1 , out, seen);
375 }
376 return out;
377 }
378
379 function deriveRecord ( name , dir ) {
380 const pj = readJson (path. join (dir, 'package.json' ));
381 const dep = pj.wix?.sdkDependency ?? {};
382 const cjs = path. join (dir, 'build' , 'cjs' );
383 const files = fs. existsSync (cjs)
384 ? fs. readdirSync (cjs). filter (( f ) => f. endsWith ( '.d.ts' ) && f !== 'meta.d.ts' )
385 : [];
386 const methods = new Map ();
387 const rawParams = new Map (); // name -> the declaration's full parameter text
388 const events = [];
389 let preview = 0 ;
390 let typeAt = null ;
391 const entityType = dep.fqdn ? pascal (dep.fqdn. split ( '.' ). pop ()) : null ;
392 const sources = [];
393
394 for ( const f of files) {
395 const file = path. join (cjs, f);
396 const src = fs. readFileSync (file, 'utf8' );
397 sources. push (src);
398 if (entityType && ! typeAt) {
399 const m = new RegExp ( `^interface ${ entityType } \\ b` , 'm' ). exec (src);
400 if (m) typeAt = { name: entityType, file, line: src. slice ( 0 , m.index). split ( ' \n ' ). length , description: descriptionBefore (src, m.index) };
401 }
402 const re = / \/\*\* ((?:(?! \*\/ ) [\s\S] ) *? ) \*\/ \s * declare function ( \w +? )(?: \$ \d + ) ? \s * \( / g ;
403 let m;
404 while ((m = re. exec (src))) {
405 const [, doc , fn ] = m;
406 const { params , ret } = readDeclaration (src, m.index + m[ 0 ]. length - 1 );
407 if ( / ^ \s * httpClient \s * :/ . test (params)) continue ; // the client-bound wrapper
408 if ( /@hidden \b| @internal \b / . test (doc)) continue ;
409 const kind = kindOf (fn, params, ret);
410 if (kind === 'event' ) {
411 if ( ! events. some (( e ) => e.name === fn)) {
412 events. push ({ name: fn, scopes: [ ...new Set ( tags (doc, 'permissionScopeId' ))] });
413 }
414 continue ;
415 }
416 const maturity = tags (doc, 'documentationMaturity' )[ 0 ];
417 if (maturity === 'preview' ) preview ++ ;
418 const prev = methods. get (fn);
419 const record = {
420 name: fn,
421 kind,
422 fqn: tags (doc, 'fqn' )[ 0 ] ?? prev?.fqn,
423 params: paramList (params),
424 returns: shorten (ret),
425 permission: tags (doc, 'permissionId' )[ 0 ] ?? prev?.permission,
426 // Only events carry @permissionScopeId today; methods get it if the SDK adds it.
427 scopes: [ ...new Set ([ ... (prev?.scopes ?? []), ... tags (doc, 'permissionScopeId' )])],
428 identities: [ ...new Set ([ ... (prev?.identities ?? []), ... tags (doc, 'applicableIdentity' )])],
429 admin: /@adminMethod \b / . test (doc) || !! prev?.admin,
430 notes: [ ...new Set ([ ... (prev?.notes ?? []), ... notesOf (doc)])],
431 required: [ ...new Set ([ ... (prev?.required ?? []), ... requiredOf (params)])],
432 maturity: maturity ? maturity. toUpperCase () : prev?.maturity ?? 'GA' ,
433 deprecated: /@deprecated \b / . test (doc)
434 ? { replacedBy: tags (doc, 'replacedBy' )[ 0 ] ?? null , removal: tags (doc, 'targetRemovalDate' )[ 0 ] ?? null }
435 : prev?.deprecated ?? null ,
436 // Where to read the parameter and return types: the public Signature
437 // below replaces this when the package has one.
438 decl: prev?.decl ?? { file, line: src. slice ( 0 , m.index + m[ 0 ]. lastIndexOf ( 'declare' )). split ( ' \n ' ). length },
439 };
440 // Overloads: keep the richer signature (a query builder beats `other`).
441 if ( ! prev || (prev.kind === 'other' && kind !== 'other' )) {
442 methods. set (fn, record);
443 rawParams. set (fn, params);
444 } else methods. set (fn, { ... prev, ... Object. fromEntries (Object. entries (record). filter (([, v ]) => v != null )), kind: prev.kind, params: prev.params, returns: prev.returns });
445 }
446 }
447
448 // The public call signature is `interface <Name>Signature { (params): Ret }`
449 // in index.d.ts. The `declare function` in index.typings.d.ts is the REST
450 // request shape (`search: CursorSearch` where the public call takes
451 // `ContactSearch`, and `options` required where it is optional), so the
452 // Signature wins whenever it exists.
453 for ( const meth of methods. values ()) {
454 const sig = `${ meth . name [ 0 ]. toUpperCase () }${ meth . name . slice ( 1 ) }Signature` ;
455 for ( const [ i , src ] of sources. entries ()) {
456 const m = new RegExp ( `interface ${ sig } \\ s* \\ {[ \\ s \\ S]*? \\ n \\ s{4} \\ (` ). exec (src);
457 if ( ! m) continue ;
458 const { params , ret } = readDeclaration (src, m.index + m[ 0 ]. length - 1 );
459 meth.decl = { file: path. join (cjs, files[i]), line: src. slice ( 0 , m.index). split ( ' \n ' ). length };
460 meth.params = paramList (params);
461 rawParams. set (meth.name, params);
462 meth.returns = shorten (ret);
463 meth.required = [ ...new Set ([ ... meth.required, ... requiredOf (params)])];
464 break ;
465 }
466 }
467
468 for ( const meth of methods. values ()) {
469 const req = [ pascal (meth.name), meth.fqn?. split ( '.' ). pop ()]
470 . filter (Boolean)
471 . map (( n ) => `${ n }Request` )
472 . find (( n ) => sources. some (( src ) => new RegExp ( `^(?:interface|type) ${ n } \\ b` , 'm' ). test (src)));
473 const limits = req ? limitsOf (sources, req, new Set ([entityType])) : [];
474 // A request field that is not a call parameter goes inside `options`.
475 // A handler gets the whole request as `payload.request`.
476 const args = meth.params. map (( p ) => p. match ( / ^ \w + / )?.[ 0 ]);
477 for ( const l of limits) {
478 if (meth.kind === 'query-builder' && / ^ query \. (paging | cursorPaging) \. limit $ / . test (l.path)) l.path = 'limit()' ;
479 else if ( / ^ payload: \w + Envelope $ / . test (meth.params. join ())) l.path = `payload.request.${ l . path }` ;
480 else if (args. includes ( 'options' ) && ! args. includes (l.path. split ( '.' )[ 0 ])) l.path = `options.${ l . path }` ;
481 }
482 const unique = [ ...new Map (limits. map (( l ) => [ JSON . stringify (l), l])). values ()];
483 if (unique. length ) meth.limits = unique;
484 }
485
486 // <Entity>SearchSpec / <Entity>QuerySpec declare what a search or query
487 // method accepts: the free-text fields and, per operator group, the fields
488 // and sort. A closed list, shipped in every generated package that has one.
489 const spec = ( kind ) => {
490 const names = entityType ? [ `${ entityType }${ kind }Spec` ] : [];
491 for ( const src of sources) {
492 const all = [ ... src. matchAll ( new RegExp ( `interface ( \\ w+)${ kind }Spec extends ${ kind }Spec \\ b` , 'g' ))]. map (( x ) => `${ x [ 1 ] }${ kind }Spec` );
493 const name = names. find (( n ) => all. includes (n)) ?? (all. length === 1 ? all[ 0 ] : null );
494 if (name) {
495 const fields = parseSpec (src, name);
496 return fields && { entity: name. slice ( 0 , - `${ kind }Spec` . length ), fields };
497 }
498 }
499 return null ;
500 };
501 const searchSpec = spec ( 'Search' );
502 const querySpec = spec ( 'Query' );
503 // The spec is one entity's (`V3ProductSearchSpec` → V3Product), so a query
504 // or search returning another type in the same namespace
505 // (services.queryPolicies) does not get it. A builder is generated per
506 // entity, so it always does.
507 const returnsEntity = ( meth , entity ) => {
508 const body = typeBody (sources, meth.returns. match ( /Promise<( \w + )/ )?.[ 1 ] ?? '' );
509 const list = new RegExp ( `^(?:Array< \\ s*${ entity } \\ b|${ entity } \\ s* \\ [ \\ ])` );
510 return ! body || membersOf (body). some (({ type }) => list. test (type. trim ()));
511 };
512 for ( const meth of methods. values ()) {
513 const s = meth.kind === 'search' ? searchSpec : / ^ query/ . test (meth.kind) ? querySpec : null ;
514 if ( ! s || (meth.kind !== 'query-builder' && ! returnsEntity (meth, s.entity))) continue ;
515 if (Object. keys (s.fields). length ) meth.filterable = s.fields;
516 else meth.filterUnlisted = true ;
517 }
518
519 const http = httpOf (path. join (cjs, 'meta.js' ));
520 for ( const meth of methods. values ()) if (http[meth.name]) meth.http = http[meth.name];
521
522 // A search method takes free text only when its search object has a
523 // `search` member (SearchDetails); some search methods take a filter only.
524 for ( const meth of methods. values ()) {
525 if (meth.kind !== 'search' ) continue ;
526 if (meth.filterable) {
527 meth.freeText = Object. values (meth.filterable). some (( f ) => f.search);
528 continue ;
529 }
530 const type = meth.params[ 0 ]?. match ( /: \s * ( \w + )/ )?.[ 1 ];
531 for ( const src of sources) {
532 const m = new RegExp ( `(?:interface ${ type } \\ b[^{]*|type ${ type } \\ s*= \\ s*) \\ {([ \\ s \\ S]*?) \\ n \\ }` ). exec (src);
533 if ( ! m) continue ;
534 meth.freeText = / ^ \s {4} search \? \s * :/ m . test (m[ 1 ]);
535 break ;
536 }
537 }
538
539 // A query builder's own methods decide what one builder call can express:
540 // the generated builders chain clauses with AND and most have no or().
541 for ( const meth of methods. values ()) {
542 if (meth.kind !== 'query-builder' ) continue ;
543 const builder = meth.returns. match ( /( \w + QueryBuilder)/ )?.[ 1 ];
544 for ( const src of sources) {
545 const m = new RegExp ( `interface ${ builder } \\ b[^{]* \\ {([ \\ s \\ S]*?) \\ n \\ }` ). exec (src);
546 if ( ! m) continue ;
547 // Each member is `op: (propertyName: 'a' | 'b', value) => Builder`: the
548 // literals are the closed list of fields that operator accepts.
549 const ops = {};
550 for ( const line of m[ 1 ]. split ( ' \n ' )) {
551 const mm = line. match ( / ^ \s {4} ( \w + ) \s * : \s * \( / );
552 if ( ! mm) continue ;
553 const params = line. slice ( 0 , line. lastIndexOf ( '=>' ));
554 const fields = [ ... params. matchAll ( /'( [ ^ '] + )'/ g )]. map (( x ) => x[ 1 ]);
555 ops[mm[ 1 ]] = /propertyNames ? \s * : \s * string/ . test (params) ? [ '*' ] : fields;
556 }
557 meth.builder = ops;
558 break ;
559 }
560 // Most builders also take one query object — `queryX({ filter, sort,
561 // cursorPaging })` — declared as the @hidden `typedQueryX`. Its filter is
562 // a WQL object, so it has $or where the builder has no or().
563 const typed = `typed${ meth . name [ 0 ]. toUpperCase () }${ meth . name . slice ( 1 ) }` ;
564 for ( const src of sources) {
565 const m = new RegExp ( `declare function ${ typed } \\ s* \\ (` ). exec (src);
566 if ( ! m) continue ;
567 const { params , ret } = readDeclaration (src, m.index + m[ 0 ]. length - 1 );
568 const form = { params: paramList (params), returns: shorten (ret) };
569 const skip = new Set ([entityType]);
570 const arg = params. match ( / ^ \s * ( \w + ) \? ? \s * : \s * ( [A-Z]\w * )/ );
571 const argShape = arg && typeShape (sources, arg[ 2 ], skip);
572 if (argShape) form.arg = `${ arg [ 1 ] }: ${ argShape }` ;
573 const retType = form.returns. match ( /Promise<( \w + )/ )?.[ 1 ];
574 const result = retType && typeShape (sources, retType, skip);
575 if (result) form.result = result;
576 // `cursorPaging` or `paging`, depending on the method.
577 form.keys = membersOf ( typeBody (sources, arg?.[ 2 ] ?? '' ) ?? '' ). map (( x ) => x.name);
578 meth.queryForm = form;
579 break ;
580 }
581 }
582
583 // The fields a search term can go in without a free-text search, declared
584 // `string` on the entity, ids and numbers in a string (`price.amount`,
585 // `taxRate`, `revision`) left out. `textFields`: those that list
586 // $startsWith. `textFieldsAny`: those under `operators: '*'` (every
587 // operator: orders' `number` takes $startsWith on a real site, although its
588 // docs page lists fewer). [] means the package lists the operators and none
589 // fits; no `textFields` means the package does not list them.
590 const entity = searchSpec?.entity ?? querySpec?.entity ?? entityType;
591 const entityBody = entity && typeBody (sources, entity);
592 // The fqdn can name the entity another way than its interface
593 // (wix.forms.v4.submission declares FormSubmission): the spec names it.
594 if ( ! typeAt && entity && entity !== entityType) {
595 const re = new RegExp ( `^interface ${ entity } \\ b` , 'm' );
596 sources. some (( src , i ) => {
597 const m = re. exec (src);
598 if (m) typeAt = { name: entity, file: path. join (cjs, files[i]), line: src. slice ( 0 , m.index). split ( ' \n ' ). length , description: descriptionBefore (src, m.index) };
599 return m;
600 });
601 }
602 const notText = ( f ) => /( ^| \. )(_ ? id | amount | value | revision) $| (Id | Count | Rate) $ / . test (f);
603 const isText = ( f ) => ! notText (f) && ( ! entityBody || fieldType (sources, entityBody, f) === 'string' );
604 for ( const meth of methods. values ()) {
605 if ( ! [ 'search' , 'query' , 'query-builder' ]. includes (meth.kind)) continue ;
606 const sw = meth.builder?.startsWith;
607 let fields;
608 let any = [];
609 if (meth.filterable) {
610 const rows = Object. entries (meth.filterable);
611 fields = rows. filter (([, f ]) => f.ops. includes ( '$startsWith' )). map (([ k ]) => k);
612 any = rows. filter (([ k , f ]) => f.ops. includes ( '*' ) && ! fields. includes (k)). map (([ k ]) => k);
613 if (sw && ! sw. includes ( '*' )) fields. push ( ... sw. filter (( f ) => ! fields. includes (f)));
614 } else if (meth.builder && ! meth.filterUnlisted && ! sw?. includes ( '*' )) fields = sw ?? [];
615 if ( ! fields) continue ;
616 meth.textFields = fields. filter (isText);
617 any = any. filter (( f ) => ! meth.textFields. includes (f) && isText (f));
618 if (any. length ) meth.textFieldsAny = any;
619 }
620
621 // A query or list method can take free text in its options, with no search
622 // method in the namespace: queryMembers(options) has `options.search:
623 // { expression, fields }`, whose fields default to one (`profile.nickname`).
624 for ( const meth of methods. values ()) {
625 if ( ! [ 'query' , 'query-builder' , 'list' ]. includes (meth.kind)) continue ;
626 for ( const p of splitParams (rawParams. get (meth.name) ?? '' )) {
627 const m = p. match ( / ^ ( \w + ) \? ? \s * : \s * ( [A-Z]\w * )/ );
628 const search = m && membersOf ( typeBody (sources, m[ 2 ]) ?? '' ). find (( x ) => x.name === 'search' );
629 const type = search?.type. match ( / ^ ( [A-Z]\w * )/ )?.[ 1 ];
630 const body = type && typeBody (sources, type);
631 if ( ! body || ! membersOf (body). some (( x ) => x.name === 'expression' )) continue ;
632 const doc = body. match ( / \/\*\* ((?:(?! \*\/ ) [\s\S] ) * ) \*\/ \s * fields \? ? \s * :/ )?.[ 1 ] ?? '' ;
633 const [ list , dflt ] = doc. split ( /Default:/ );
634 meth.optionsSearch = {
635 param: m[ 1 ],
636 fields: [ ... list. matchAll ( /`( [\w.] + )`/ g )]. map (( x ) => x[ 1 ]),
637 default: dflt?. match ( /`( [\w.] + )`/ )?.[ 1 ] ?? null ,
638 max: Number (doc. match ( /@maxSize ( \d + )/ )?.[ 1 ]) || null ,
639 };
640 break ;
641 }
642 }
643
644 // The keys of a search, query or list method's argument and response, so a
645 // caller does not have to find `ContactSearch` (an empty `{}` placeholder in
646 // index.typings.d.ts, the real `type ContactSearch = {` elsewhere).
647 // A fieldsets option decides which fields come back: queryMembers defaults to
648 // PUBLIC, without loginEmail or contact, so a table of names and emails
649 // renders empty columns unless it asks for FULL. The enum's JSDoc lists the
650 // fields of each set.
651 for ( const meth of methods. values ()) {
652 for ( const p of splitParams (rawParams. get (meth.name) ?? '' )) {
653 const t = p. match ( / ^ \w + \? ? \s * : \s * ( [A-Z]\w * )/ )?.[ 1 ];
654 const f = t && /(?: \/\*\* ((?:(?! \*\/ ) [\s\S] ) *? ) \*\/ \s * ) ? fieldsets \? ? \s * : \s * ( [A-Z]\w *? )(?:WithLiterals) ? \[\] / . exec ( typeBody (sources, t) ?? '' );
655 const enumBody = f && sources. map (( s ) => new RegExp ( `declare enum ${ f [ 2 ] } \\ {([ \\ s \\ S]*?) \\ n \\ }` ). exec (s)?.[ 1 ]). find (Boolean);
656 const values = [ ... (enumBody ?? '' ). matchAll ( / \/\*\* ((?:(?! \*\/ ) [\s\S] ) *? ) \*\/ \s * ( \w + ) =/ g )]
657 . map (([, doc , name ]) => {
658 const text = jsdocLine (doc);
659 // The top-level fields the set returns, from its "Includes …" sentence
660 // only (PUBLIC's next sentence names fields it returns as UNKNOWN).
661 const includes = / \b all fields \b / i . test (text)
662 ? 'all'
663 : [ ... (text. match ( / \b Includes ( [ ^ .] * )/ )?.[ 1 ] ?? '' ). matchAll ( /`( [\w.] + )`/ g )]. map (( x ) => x[ 1 ]);
664 return { name, fields: text, includes };
665 });
666 if ( ! values. length ) continue ;
667 meth.fieldsets = {
668 default: (f[ 1 ] ?? '' ). match ( /Default: \s * ` ? " ? ( \w + )/ )?.[ 1 ] ?? null ,
669 values,
670 // The entity's top-level fields, to name the ones only an "all fields" set returns.
671 entityFields: membersOf (entityBody ?? '' ). map (( x ) => x.name. replace ( / ^ _/ , '' )),
672 };
673 break ;
674 }
675 }
676
677 for ( const meth of methods. values ()) {
678 if ( ! [ 'search' , 'query' , 'query-builder' , 'list' ]. includes (meth.kind)) continue ;
679 const skip = new Set ([entityType]);
680 // The first parameter with an object type (`contactId: string` has no keys).
681 for ( const p of splitParams (rawParams. get (meth.name) ?? '' )) {
682 const m = p. match ( / ^ ( \w + ) \? ? \s * : \s * (?:NonNullablePaths< \s * ) ? ( [A-Z]\w * )/ );
683 const shape = m && typeShape (sources, m[ 2 ], skip);
684 if (shape) {
685 meth.arg = `${ m [ 1 ] }: ${ shape }` ;
686 break ;
687 }
688 }
689 const retType = meth.returns. match ( /Promise<( \w + )/ )?.[ 1 ];
690 if (retType) meth.result = typeShape (sources, retType, skip);
691 }
692
693 return {
694 package: name,
695 version: pj.version,
696 fqdn: dep.fqdn ?? null ,
697 fqdnNamespace: dep.fqdnNamespace ?? null ,
698 maturity: methods.size && preview === methods.size ? 'PREVIEW' : 'GA' ,
699 methods: [ ... methods. values ()],
700 events,
701 type: typeAt,
702 };
703 }
704
705 // interface XSearchSpec extends SearchSpec {
706 // searchable: ['a', 'b'];
707 // wql: [{ operators: ['$eq', …]; fields: ['c', …]; sort: 'BOTH' | 'ASC' | 'DESC' | 'NONE' }, …];
708 // } → { field: { ops: [...], sort: bool, search: bool } }
709 function parseSpec ( src , name ) {
710 const m = new RegExp ( `interface ${ name } extends \\ w+Spec \\ s* \\ {([ \\ s \\ S]*?) \\ n \\ }` ). exec (src);
711 if ( ! m) return null ;
712 const body = m[ 1 ];
713 const list = ( s ) => [ ... (s ?? '' ). matchAll ( /'( [ ^ '] + )'/ g )]. map (( x ) => x[ 1 ]);
714 const out = {};
715 const field = ( f ) => (out[f] ??= { ops: [], sort: false , search: false });
716 for ( const f of list (body. match ( /searchable: \s * \[ ( [\s\S] *? ) \] / )?.[ 1 ])) field (f).search = true ;
717 const wql = body. slice (body. indexOf ( 'wql:' ));
718 for ( const block of wql. matchAll ( / \{ ( [ ^ {}] * ) \} / g )) {
719 const ops = list (block[ 1 ]. match ( /operators: \s * ( \[ [ ^ \] ] * \] | ' [ ^ '] * ')/ )?.[ 1 ]);
720 const sort = block[ 1 ]. match ( /sort: \s * '( \w + )'/ )?.[ 1 ] ?? 'NONE' ;
721 for ( const f of list (block[ 1 ]. match ( /fields: \s * \[ ( [ ^ \] ] * ) \] / )?.[ 1 ])) {
722 const e = field (f);
723 e.ops = [ ...new Set ([ ... e.ops, ... ops])];
724 e.sort = e.sort || sort !== 'NONE' ;
725 }
726 }
727 // `{}` when the spec is declared but lists no field (`wql: []`): the
728 // filter fields are on the method's docs page only.
729 return out;
730 }
731
732 function httpOf ( metaFile ) {
733 if ( ! fs. existsSync (metaFile)) return {};
734 const src = fs. readFileSync (metaFile, 'utf8' );
735 const out = {};
736 const re = /function ( \w +? )2 \(\) \{ [\s\S] *? httpMethod: "( \w + )", \s * path: "( [ ^ "] + )"/ g ;
737 let m;
738 while ((m = re. exec (src))) out[m[ 1 ]] = `${ m [ 2 ] } ${ m [ 3 ] }` ;
739 return out;
740 }
741
742
743 // --- matching and ranking ----------------------------------------------------
744
745 const norm = ( s ) => s. toLowerCase (). replace ( / [ ^ a-z0-9] / g , '' );
746 function variants ( term ) {
747 const t = norm (term);
748 const out = new Set ([t, `${ t }s` ]);
749 if (t. endsWith ( 'ies' )) out. add ( `${ t . slice ( 0 , - 3 ) }y` );
750 if (t. endsWith ( 's' )) out. add (t. slice ( 0 , - 1 ));
751 return out;
752 }
753 const versionOf = ( fqdn ) => Number ((fqdn ?? '' ). match ( / \. v( \d + ) \. / )?.[ 1 ] ?? 0 );
754 const baseOf = ( ns ) => norm (ns. replace ( /V \d +$ / , '' ));
755
756 function namespacesOf ( name , entry ) {
757 if (entry.exportedAs. length ) return entry.exportedAs;
758 return [{ from: name, namespace: null }];
759 }
760
761 function loadCurated () {
762 const file = path. join (__dirname, 'sdk-curated.json' );
763 return fs. existsSync (file) ? readJson (file).rules : [];
764 }
765
766 function moduleMapRow ( term ) {
767 const file = path. join (__dirname, '..' , 'references' , 'SDK_MODULE_MAP.md' );
768 if ( ! fs. existsSync (file)) return null ;
769 const v = variants (term);
770 for ( const line of fs. readFileSync (file, 'utf8' ). split ( ' \n ' )) {
771 const m = line. match ( / ^ \| \s * ( [ ^ |] +? ) \s * \| \s * `(@wix \/ [\w-] + )`/ );
772 if ( ! m) continue ;
773 const words = m[ 1 ]. split ( / [\s/,] + / ). map (norm);
774 if (words. some (( w ) => v. has (w))) return { entities: m[ 1 ], package: m[ 2 ] };
775 }
776 return null ;
777 }
778
779 function lookup ( term , intent , pkgs , curated ) {
780 const v = variants (term);
781 const primary = [];
782 const related = [];
783 for ( const [ name , entry ] of pkgs.autos) {
784 const pj = readJson (path. join (entry.dir, 'package.json' ));
785 const dep = pj.wix?.sdkDependency ?? {};
786 const last = norm ((dep.fqdn ?? '' ). split ( '.' ). pop () ?? '' );
787 for ( const ns of namespacesOf (name, entry)) {
788 const nsBase = ns.namespace ? baseOf (ns.namespace) : norm (name. replace ( / ^ @wix \/ auto_sdk_ [ ^ _] + _/ , '' ));
789 const hay = norm ( `${ name } ${ dep . fqdn ?? ''} ${ ns . namespace ?? ''}` );
790 if (v. has (nsBase) || v. has (last)) primary. push ({ name, entry, ns });
791 else if ([ ... v]. some (( x ) => x. length > 3 && hay. includes (x))) related. push ({ name, entry, ns });
792 }
793 }
794
795 const rules = curated. filter (( r ) => r.entities. some (( e ) => v. has ( norm (e))));
796 // A curated preference can name a namespace the term itself does not match
797 // (a refunds table reads orders), so pull that namespace in.
798 const prefer = rules. find (( r ) => r.choice?.prefer && ( ! r.choice.intents || r.choice.intents. includes (intent)));
799 const siteRule = rules. find (( r ) => r.choice === 'site' );
800 const pullIn = [prefer?.choice.prefer, ... (siteRule?.namespaces ?? [])]. filter (Boolean);
801 for ( const want of pullIn) {
802 if (primary. some (( c ) => c.ns.namespace === want)) continue ;
803 for ( const [ name , entry ] of pkgs.autos) {
804 const ns = entry.exportedAs. find (( x ) => x.namespace === want);
805 if (ns) primary. push ({ name, entry, ns });
806 }
807 }
808 if ( ! primary. length ) {
809 const row = moduleMapRow (term);
810 if (row && ! pkgs.wrappers. has (row.package)) return { term, status: 'not-installed' , row, rules };
811 return { term, status: 'unknown' , near: nearMatches (term, pkgs), related: related. map (label), rules };
812 }
813
814 const candidates = primary. map (( c ) => ({ ... c, record: deriveRecord (c.name, c.entry.dir) }));
815 const reasons = new Map ();
816 const why = ( c , r ) => reasons. set (c, [ ... (reasons. get (c) ?? []), r]);
817 for ( const c of candidates) {
818 why (c, `fqdn v${ versionOf ( c . record . fqdn ) } (${ c . record . fqdn })` );
819 if (c.record.maturity === 'PREVIEW' ) why (c, 'preview' );
820 if (c.record.methods. length && c.record.methods. every (( m ) => m.deprecated)) why (c, 'deprecated' );
821 if ( searching (intent)) why (c, c.record.methods. some (( m ) => m.kind === 'search' ) ? 'has search' : 'no search method' );
822 if ( ! c.ns.namespace) why (c, 'not exported from a public package root (SPI or internal)' );
823 if (c.ns.installed === false ) why (c, `${ c . ns . from } not installed` );
824 }
825
826 const preferRule = prefer && candidates. some (( c ) => c.ns.namespace === prefer.choice.prefer) ? prefer : null ;
827 // Several packages can export the same namespace (orders: ecom, pricing-plans,
828 // events). Across packages the module map's package wins; with no map row,
829 // the package named after the namespace (@wix/categories, not @wix/blog),
830 // then a package whose entity has a description; then the package name, so
831 // the order never depends on which package is read first. The API
832 // version and a search method rank only the namespaces of one package
833 // (contacts vs contactsV5): across packages they say nothing about the feature.
834 const packages = new Set (candidates. filter (( c ) => c.ns.namespace). map (( c ) => c.ns.from));
835 const mapPackage = packages.size > 1 ? moduleMapRow (term)?.package : null ;
836 const namesake = ( c ) =>
837 packages.size > 1 && !! c.ns.namespace && norm (c.ns.from. replace ( / ^ @wix \/ / , '' )) === baseOf (c.ns.namespace);
838 // Per package, not per namespace, so it never reorders one package's namespaces.
839 const described = new Set (candidates. filter (( c ) => c.record.type?.description). map (( c ) => c.ns.from));
840 const score = ( c ) => [
841 c.record.maturity === 'PREVIEW' || reasons. get (c). includes ( 'deprecated' ) ? 0 : 1 ,
842 preferRule && c.ns.namespace === preferRule.choice.prefer ? 1 : 0 ,
843 c.ns.namespace ? 1 : 0 ,
844 mapPackage && c.ns.from === mapPackage ? 1 : 0 ,
845 namesake (c) ? 1 : 0 ,
846 packages.size > 1 && described. has (c.ns.from) ? 1 : 0 ,
847 ];
848 const withinPackage = ( c ) => [
849 searching (intent) && c.record.methods. some (( m ) => m.kind === 'search' ) ? 1 : 0 ,
850 versionOf (c.record.fqdn),
851 ];
852 const higher = ( x , y ) => x. findIndex (( v , i ) => v !== y[i]);
853 candidates. sort (( a , b ) => {
854 let i = higher ( score (a), score (b));
855 if (i >= 0 ) return score (b)[i] - score (a)[i];
856 if (a.ns.from !== b.ns.from) return (a.ns.from ?? '' ). localeCompare (b.ns.from ?? '' );
857 i = higher ( withinPackage (a), withinPackage (b));
858 if (i >= 0 ) return withinPackage (b)[i] - withinPackage (a)[i];
859 return (a.ns.namespace ?? '' ). localeCompare (b.ns.namespace ?? '' );
860 });
861 if (preferRule) why (candidates[ 0 ], 'curated' );
862 else if (mapPackage && candidates[ 0 ].ns.from === mapPackage) why (candidates[ 0 ], 'the module map package' );
863 else if ( namesake (candidates[ 0 ])) why (candidates[ 0 ], 'the package named after it' );
864 if (packages.size > 1 ) {
865 for ( const c of candidates. filter (( c ) => c.ns.namespace)) c.description = c.record.type?.description ?? null ;
866 }
867 return {
868 term,
869 status: 'found' ,
870 siteDependent: !! siteRule,
871 candidates: candidates. map (( c ) => ({
872 ... c,
873 reasons: reasons. get (c),
874 site: !! siteRule && siteRule.namespaces. includes (c.ns.namespace),
875 })),
876 related: related. filter (( r ) => ! primary. some (( p ) => p.name === r.name)). map (label),
877 rules,
878 };
879 }
880
881 const label = ( c ) => (c.ns.namespace ? `${ c . ns . namespace } (${ c . ns . from })` : c.name);
882
883 // The call-shape rule: which one call to write for the intent, and why.
884 // search: a search method with free text → one call, the term in
885 // search.search.expression. Otherwise one filter object with $or,
886 // one clause per field. A query builder chains clauses with AND and
887 // has no or(): when the method also takes a query object, its
888 // filter has $or; else one field with startsWith, never one query
889 // per field merged in the client.
890 // read: the builder for filters, sort and paging, on the fields its
891 // operators list (a closed list), else the filter object. A
892 // builder with no filter or sort (extendedBookings) → the query
893 // object.
894 // write: a method that requires revision → read the entity first.
895 function shapeFor ( record , ns , intent ) {
896 // `all` holds everything `search` and `write` hold: an agent that picks it for a
897 // search table otherwise gets no shape line and re-derives the call from the typings.
898 if (intent === 'all' ) return [ ... shapeFor (record, ns, 'search' ), ... shapeFor (record, ns, 'write' )];
899 // Not the trash-bin reads (queryDeletedForms): a search is over live items.
900 const by = ( k ) => record.methods. filter (( m ) => m.kind === k && ! m.deprecated && ! /Deleted | TrashBin/ . test (m.name));
901 const [ search ] = by ( 'search' );
902 const [ builder ] = by ( 'query-builder' );
903 const [ query ] = by ( 'query' ). filter (( m ) => m.name. toLowerCase (). includes ( baseOf (ns ?? '' )));
904 const call = ( m ) => `${ ns ?? 'sdk'}.${ m . name }` ;
905 const textFields = ( m ) =>
906 m?.filterable ? Object. entries (m.filterable). filter (([, f ]) => f.search). map (([ k ]) => k) : null ;
907 const queryCall = ( m ) => `${ call ( m ) }({ ${ ( m . queryForm . keys ?. length ? m . queryForm . keys : [ 'filter' , 'sort' ]). join ( ', ' ) } })` ;
908 const out = [];
909 let opt;
910
911 if (intent === 'search' ) {
912 if (search && search.freeText !== false ) {
913 const fields = textFields (search);
914 out. push ( `${ call ( search ) }({ search: { expression: term } }): one call${ fields ? `; the term matches ${ fields . join ( ', ' ) }` : ''}; exact-match facets go in filter` );
915 } else if ((opt = [builder, query, ... by ( 'list' )]. find (( m ) => m?.optionsSearch))) {
916 // Free text in a query's options, with no search method (queryMembers).
917 const o = opt.optionsSearch;
918 const first = splitParams (opt.params. join ( ', ' ))[ 0 ]?. startsWith (o.param);
919 const arg = `{ search: { expression: term, fields } }` ;
920 out. push ( `${ call ( opt ) }(${ first ? arg : `…, ${ arg }`})${ opt . kind === 'query-builder' ? '.find()' : ''}: no search method; free text in its ${ o . param }.search` );
921 if (o.default) out. push ( ` search.fields defaults to ${ o . default } alone: name every field the term must match${ o . max ? ` (at most ${ o . max })` : ''}` );
922 if (o.fields. length ) out. push ( ` searchable fields: ${ o . fields . join ( ', ' ) }` );
923 out. push ( ' never run one query per field and merge the results in the client' );
924 } else {
925 // Without free text, a term goes in $startsWith on the text fields: a
926 // prefix match. None of them → the server cannot match a term at all.
927 const orBuilder = builder?.builder?.or;
928 const form = search ?? query ?? (builder?.queryForm && ! orBuilder ? builder : null );
929 const target = form ?? builder;
930 const listed = target?.textFields;
931 const any = target?.textFieldsAny ?? [];
932 const fields = listed && [ ... listed, ... any];
933 const where = form === builder ? queryCall (builder) : target && `${ call ( target ) }${ target === builder ? '()' : ''}` ;
934 const merge = ' never run one query per field and merge the results in the client' ;
935 if (fields && ! fields. length ) {
936 // A search argument can carry `search: { expression }` (the generic
937 // search object) while the spec declares no searchable field: the
938 // type accepts a term the service has no field to match it on.
939 const typed = search && / \b search: \{ [ ^ }] *\b expression \b / . test (search.arg ?? '' );
940 out. push ( `${ where }: no text match in the package: no searchable field, and no text field takes $startsWith` );
941 if (typed) out. push ( ' its argument types search.expression, but its search spec declares no searchable field: do not rely on it unless the docs page lists searchable fields' );
942 out. push ( ` next: its docs page (SearchWixSDKDocumentation "${ target . name }"), not the typings: nothing more is in the package` );
943 out. push ( ' if the docs list no text filter either, tell the user the API cannot match a search term;' );
944 out. push ( ' a search then reads every page (cursor) and matches the values itself, re-read on each search, with no silent cap' );
945 out. push ( ' never match only the loaded page, or a capped or cached set, and present it as a search' );
946 } else if (form) {
947 const why = form === builder ? 'no search method, and the builder has no or(): pass one query object instead' : search ? 'its search takes no free text' : 'no search method' ;
948 out. push ( `${ where }: ${ why }` );
949 if (form === search) out. push ( ' a search.expression here can be ignored with no error (on a real site, searchOrders returned every order): put the term in filter' );
950 const or = fields && `filter: { $or: [${ fields . slice ( 0 , 2 ). map (( f ) => `{ '${ f }': { $startsWith: term } }` ). join ( ', ' ) }${ fields . length > 2 ? ', …' : ''}] }` ;
951 // The package has nothing more to find when it lists no fields: say so,
952 // or the agent greps the typings and the compiled JS for operators.
953 if ( ! fields) {
954 out. push ( ' the term: filter $or, one clause per text field — the package has no filter list for this method' );
955 out. push ( ` next: its docs page (SearchWixSDKDocumentation "${ target . name } filter"), not the typings or the compiled JS: nothing more is in the package` );
956 out. push ( ' the docs page lists the fields and operators: $contains where a field takes it (a match inside a value), else $startsWith (a prefix)' );
957 } else {
958 out. push (listed. length ? ` the term: ${ or } on the text fields ${ capped ( listed ) }` : ` the term: ${ or }` );
959 if (any. length ) {
960 out. push ( ` ${ listed . length ? 'also ' : ''}text fields with operators '*' (every operator, $startsWith too): ${ capped ( any ) }` );
961 }
962 out. push ( ' $startsWith is a prefix match; the package lists $contains for no field: for a match inside a value, check the method docs page' );
963 }
964 if (form === builder) out. push ( ' an API may implement only part of WQL: if its docs page rules out $or, use ONE field and say so to the user' );
965 out. push (merge);
966 } else if (orBuilder) {
967 out. push ( `${ where }: no search method; the builder has or(), so chain one .startsWith() per field inside .or()${ fields ? ` (${ capped ( fields ) })` : ''}` );
968 } else if (builder) {
969 out. push ( `${ where }: no search method, and the builder chains clauses with AND and has no or()` );
970 out. push ( ` so one builder call cannot match a term in any of several fields: use startsWith on ONE field${ fields ?. length ? ` (${ capped ( fields ) })` : ''}, and say so to the user` );
971 out. push (merge);
972 }
973 }
974 } else if (intent === 'read' ) {
975 if (builder) {
976 const ops = Object. entries (builder.builder ?? {}). filter (([ k , f ]) => f. length && ! [ 'limit' , 'skip' , 'skipTo' , 'find' ]. includes (k));
977 if ( ! ops. length && builder.queryForm) {
978 out. push ( `${ queryCall ( builder ) }: the builder has no filter or sort, so pass one query object` );
979 out. push ( ` fields and operators: ${ builder . filterable ? 'its filter list' : 'its docs page (the package does not list them)'}` );
980 } else {
981 out. push ( `${ call ( builder ) }(): filters, sort and paging chained on the builder (AND only); fields per operator are a closed list:` );
982 for ( const [ op , f ] of ops) out. push ( ` .${ op }(${ f . includes ( '*' ) ? 'any field' : capped ( f , 10 ) })` );
983 if (builder.queryForm) out. push ( ` or one query object, ${ queryCall ( builder ) }, whose filter also takes $or` );
984 }
985 if (search) out. push ( ` a search box over several fields → ${ call ( search ) } instead (--intent search)` );
986 } else if (search || query) {
987 out. push ( `${ call ( search ?? query ) }: one filter object (fields from its filter list or its docs page)` );
988 }
989 } else if (intent === 'write' ) {
990 const needsRevision = record.methods. filter (( m ) => / ^ (update | delete)/ . test (m.kind) && m.required. some (( r ) => /revision $ / . test (r)));
991 if (needsRevision. length ) {
992 out. push ( `${ needsRevision . map ( call ). join ( ', ' ) } require revision: read the entity first and pass its current revision (a stale one fails)` );
993 }
994 }
995 return out;
996 }
997
998 function nearMatches ( term , pkgs ) {
999 const t = norm (term);
1000 const names = new Set ();
1001 for ( const [ name , entry ] of pkgs.autos) for ( const ns of namespacesOf (name, entry)) names. add (ns.namespace ?? name);
1002 const dist = ( a , b ) => {
1003 const d = Array. from ({ length: a. length + 1 }, ( _ , i ) => [i, ... Array (b. length ). fill ( 0 )]);
1004 for ( let j = 1 ; j <= b. length ; j ++ ) d[ 0 ][j] = j;
1005 for ( let i = 1 ; i <= a. length ; i ++ )
1006 for ( let j = 1 ; j <= b. length ; j ++ )
1007 d[i][j] = Math. min (d[i - 1 ][j] + 1 , d[i][j - 1 ] + 1 , d[i - 1 ][j - 1 ] + (a[i - 1 ] === b[j - 1 ] ? 0 : 1 ));
1008 return d[a. length ][b. length ];
1009 };
1010 return [ ... names]
1011 . map (( n ) => [n, norm (n). includes (t) || t. includes ( baseOf (n)) ? 0 : dist (t, baseOf (n))])
1012 . filter (([, d ]) => d <= 3 )
1013 . sort (( a , b ) => a[ 1 ] - b[ 1 ])
1014 . slice ( 0 , 5 )
1015 . map (([ n ]) => n);
1016 }
1017
1018 // --- output ------------------------------------------------------------------
1019
1020 // filterable: { field: { ops: [...], sort: bool, search: bool } } — a closed
1021 // list per method, so a field that is not here cannot be filtered on.
1022 // The smallest set for each field, computed, not left to the agent: a run that
1023 // read "EXTENDED: … and the `profile` object" chose EXTENDED for a name column,
1024 // and EXTENDED has no `contact`. Sets the package does not describe by field
1025 // (forms: NESTED_FORMS) print their own text.
1026 function renderFieldsets ( m ) {
1027 const { default : dflt , values , entityFields } = m.fieldsets;
1028 if ( ! values. some (( v ) => Array. isArray (v.includes) && v.includes. length )) {
1029 return values. map (( v ) => `${ v . name }: ${ v . fields }` ). join ( ' · ' );
1030 }
1031 const seen = new Set ();
1032 const parts = values. map (( v ) => {
1033 if (v.includes === 'all' ) {
1034 const only = entityFields. filter (( f ) => ! seen. has (f));
1035 return `${ v . name }: every field${ only . length ? ` — only here: ${ only . join ( ', ' ) }` : ''}` ;
1036 }
1037 const added = v.includes. filter (( f ) => ! seen. has (f));
1038 added. forEach (( f ) => seen. add (f));
1039 return `${ v . name }${ v === values [ 0 ] ? '' : ' adds'}: ${ added . join ( ', ' ) }` ;
1040 });
1041 return `${ dflt ? `default ${ dflt }; ` : ''}ask for the smallest set that returns every field you show — ${ parts . join ( ' · ' ) }` ;
1042 }
1043
1044 function renderFilterable ( filterable ) {
1045 const rows = Object. entries (filterable);
1046 const text = rows. filter (([, f ]) => f.search). map (([ k ]) => k);
1047 const sort = rows. filter (([, f ]) => f.sort). map (([ k ]) => k);
1048 const byOps = new Map ();
1049 for ( const [ k , f ] of rows) {
1050 const ops = (f.ops ?? []). join ( ' ' );
1051 // A free-text-only field (contacts' `name.full`) has no operators: the free-text line covers it.
1052 if ( ! ops) continue ;
1053 byOps. set (ops, [ ... (byOps. get (ops) ?? []), k]);
1054 }
1055 const out = [];
1056 if (text. length ) out. push ( `free text: ${ text . join ( ', ' ) }` );
1057 for ( const [ ops , fields ] of byOps) out. push ( `filter ${ ops }: ${ capped ( fields , 12 ) }` );
1058 if (sort. length ) out. push ( `sort: ${ capped ( sort , 12 ) }` );
1059 out. push ( '(closed list: a field not shown cannot be filtered or sorted on)' );
1060 return out;
1061 }
1062
1063 const capped = ( list , n = 8 ) => (list. length > n ? `${ list . slice ( 0 , n ). join ( ', ' ) }, +${ list . length - n } more` : list. join ( ', ' ));
1064
1065 function renderLimit ( l ) {
1066 const range = ( lo , hi ) => (hi == null ? `≥${ lo }` : lo == null ? `≤${ hi }` : `${ lo }–${ hi }` );
1067 return l.minSize != null || l.maxSize != null ? `${ l . path } ${ range ( l . minSize , l . maxSize ) } items` : `${ l . path } ${ range ( l . min , l . max ) }` ;
1068 }
1069
1070 function render ( result , intent , project ) {
1071 const out = [ `== ${ result . term } (intent: ${ intent })` ];
1072 const rel = ( f ) => path. relative (project, f) || f;
1073 if (result.status === 'not-installed' ) {
1074 out. push ( `not installed: "${ result . row . entities }" → ${ result . row . package }` );
1075 out. push ( ` npm i ${ result . row . package } then run this lookup again` );
1076 } else if (result.status === 'unknown' ) {
1077 out. push ( 'no installed SDK namespace matches this name' );
1078 if (result.near. length ) out. push ( ` near: ${ result . near . join ( ', ' ) }` );
1079 if (result.related. length ) out. push ( ` related: ${ capped ( result . related ) }` );
1080 out. push ( ' if it is a Wix business entity, check references/SDK_MODULE_MAP.md, then search the SDK docs' );
1081 } else {
1082 // A site-dependent entity has one namespace per site kind, all shown in full.
1083 const chosen = result.siteDependent ? result.candidates. filter (( c ) => c.site) : result.candidates. slice ( 0 , 1 );
1084 const lead = result.siteDependent ? 'site' : 'use' ;
1085 // Packages that export the same namespace: each one's entity description,
1086 // so the agent switches when the feature matches another package.
1087 const what = ( c ) =>
1088 c.description === undefined ? '' : c.description ? ` — "${ c . description }"` : ' — no description in the package' ;
1089 for ( const c of chosen) out. push ( `${ lead . padEnd ( 5 ) } ${ label ( c ) } — ${ c . reasons . join ( '; ' ) }${ what ( c ) }` );
1090 for ( const c of result.candidates. filter (( c ) => ! chosen. includes (c))) {
1091 out. push ( `also ${ label ( c ) } — ${ c . reasons . join ( '; ' ) }${ what ( c ) }` );
1092 }
1093 if (result.candidates. some (( c ) => c.description !== undefined )) {
1094 out. push ( ' several packages export this name: if the feature matches another package \' s description, use that one' );
1095 }
1096 for ( const c of chosen) out. push ( ... renderCandidate (c, intent, rel));
1097 if (result.related. length ) out. push ( `related: ${ capped ( result . related ) }` );
1098 }
1099 for ( const r of result.rules) out. push ( `note ${ r . note } [${ r . source }]` );
1100 return out. join ( ' \n ' );
1101 }
1102
1103 function renderCandidate ( c , intent , rel ) {
1104 const r = c.record;
1105 const ns = c.ns.namespace;
1106 const out = [];
1107 if ( ! ns) out. push ( `// ${ c . name } is not exported from a public package root; check its wrapper's subpath exports` );
1108 else if (c.ns.installed === false ) out. push ( `// ${ c . ns . from } is not installed (only pulled in by another package): npm i ${ c . ns . from }` );
1109 out. push (ns ? `import { ${ ns } } from '${ c . ns . from }';` : `import * as sdk from '${ c . name }';` );
1110 // Before the methods: a long method list must not push the declaration file
1111 // past an agent's `| head`.
1112 if (r.type) out. push ( ` type ${ r . type . name } ${ rel ( r . type . file ) }:${ r . type . line }` );
1113 out. push ( ` package ${ r . package }@${ r . version }` );
1114 const shape = shapeFor (r, ns, intent);
1115 if (shape. length ) out. push ( `shape ${ shape [ 0 ] }` , ... shape. slice ( 1 ). map (( l ) => ` ${ l }` ));
1116 const kinds = INTENTS [intent];
1117 const shown =
1118 intent === 'event'
1119 ? []
1120 : r.methods
1121 . filter (( m ) => ! kinds || kinds. includes (m.kind))
1122 . sort (( a , b ) => {
1123 const order = kinds ?? ALL_ORDER ;
1124 const rank = ( m ) => (order. includes (m.kind) ? order. indexOf (m.kind) : order. length );
1125 return rank (a) - rank (b);
1126 });
1127 for ( const m of shown) {
1128 const call = `${ ns ?? 'sdk'}.${ m . name }(${ m . params . join ( ', ' ) }) → ${ m . returns }` ;
1129 out. push ( ` ${ m . kind . padEnd ( 13 ) } ${ call }` );
1130 // @applicableIdentity is APP on visitor methods too (addLineItemsToCurrentCart,
1131 // getCurrentMember), so it says nothing about who may call: not printed. The
1132 // JSDoc notes say it when the docs page does.
1133 const facts = [ `permission ${ m . permission ?? '?'}` ];
1134 facts. push (m.scopes?. length ? `scope ${ m . scopes . join ( ' | ' ) }` : 'scope: not in the package (the method docs page lists it)' );
1135 if (m.admin) facts. push ( 'admin method' );
1136 if (m.required. length ) facts. push ( `required ${ m . required . join ( ', ' ) }` );
1137 if (m.maturity !== 'GA' ) facts. push (m.maturity. toLowerCase ());
1138 if (m.deprecated) facts. push ( `DEPRECATED${ m . deprecated . replacedBy ? ` → ${ m . deprecated . replacedBy }` : ''}` );
1139 out. push ( ` ${'' . padEnd ( 13 ) } ${ facts . join ( ' · ' ) }` );
1140 for ( const n of m.notes ?? []) out. push ( ` ${'' . padEnd ( 13 ) } note ${ n }` );
1141 if (m.limits) out. push ( ` ${'' . padEnd ( 13 ) } limits ${ m . limits . map ( renderLimit ). join ( ' · ' ) }` );
1142 if (m.arg) out. push ( ` ${'' . padEnd ( 13 ) } arg ${ m . arg }` );
1143 if (m.fieldsets) out. push ( ` ${'' . padEnd ( 13 ) } fieldsets ${ renderFieldsets ( m ) }` );
1144 if (m.result) out. push ( ` ${'' . padEnd ( 13 ) } returns ${ m . result }` );
1145 if (m.queryForm) {
1146 const q = m.queryForm;
1147 out. push ( ` ${'' . padEnd ( 13 ) } or ${ ns ?? 'sdk'}.${ m . name }(${ q . params . join ( ', ' ) }) → ${ q . returns }` );
1148 if (q.arg) out. push ( ` ${'' . padEnd ( 13 ) } arg ${ q . arg }` );
1149 if (q.result) out. push ( ` ${'' . padEnd ( 13 ) } returns ${ q . result }` );
1150 }
1151 if (m.filterable) out. push ( ... renderFilterable (m.filterable). map (( l ) => ` ${'' . padEnd ( 13 ) } ${ l }` ));
1152 else if (m.filterUnlisted) out. push ( ` ${'' . padEnd ( 13 ) } filter fields: not in the package (the method docs page lists them)` );
1153 // The declaration, for the types behind `arg` and `returns`: the public
1154 // call is in index.d.ts, not in the hashed file the entity type is in.
1155 const decl = m.decl && `decl ${ rel ( m . decl . file ) }:${ m . decl . line }` ;
1156 if (m.fqn || decl) out. push ( ` ${'' . padEnd ( 13 ) } ${ [ m . fqn && `fqn ${ m . fqn }` , decl ]. filter ( Boolean ). join ( ' · ' ) }` );
1157 }
1158 const others = r.methods. filter (( m ) => ! shown. includes (m)). map (( m ) => m.name);
1159 if (others. length && intent !== 'all' ) {
1160 const next = intent === 'write' ? '--intent all' : '--intent write (or all)' ;
1161 out. push ( ` other methods (params, permission, scope: run again with ${ next }): ${ others . join ( ', ' ) }` );
1162 }
1163 if (intent === 'event' || intent === 'all' ) {
1164 for ( const e of r.events) out. push ( ` event ${ ns ?? 'sdk'}.${ e . name }(handler) · scope ${ e . scopes . join ( ' | ' ) || '?'}` );
1165 } else if (r.events. length ) {
1166 out. push ( ` events: ${ r . events . map (( e ) => e . name ). join ( ', ' ) }` );
1167 }
1168 return out;
1169 }
1170
1171 // --- main --------------------------------------------------------------------
1172
1173 function main ( argv ) {
1174 const args = [ ... argv];
1175 const flag = ( n ) => {
1176 const i = args. indexOf (n);
1177 return i >= 0 ? args. splice (i, 2 )[ 1 ] : undefined ;
1178 };
1179 const intent = flag ( '--intent' ) ?? 'read' ;
1180 const project = path. resolve ( flag ( '--project' ) ?? process. cwd ());
1181 const asJson = args. includes ( '--json' );
1182 const terms = args. filter (( a ) => ! a. startsWith ( '--' ));
1183 if ( ! terms. length || ! (intent in INTENTS )) {
1184 console. error ( 'usage: sdk-lookup.cjs <entity> [<entity> ...] [--intent search|read|write|event|all] [--json]' );
1185 return 2 ;
1186 }
1187 activatePnp (project);
1188 let pkgs;
1189 try {
1190 pkgs = findPackages (project);
1191 } catch (e) {
1192 if ( ! (e instanceof LookupError )) throw e;
1193 console. error ( `sdk-lookup: ${ e . message }` );
1194 return 4 ;
1195 }
1196 if ( ! pkgs.autos.size) {
1197 console. error ( `no @wix/auto_sdk_* packages found from ${ project }; install the vertical's @wix/<package> first` );
1198 return 3 ;
1199 }
1200 const curated = loadCurated ();
1201 const results = terms. map (( t ) => lookup (t, intent, pkgs, curated));
1202 if (asJson) {
1203 const plain = results. map (( r ) => ({
1204 ... r,
1205 candidates: r.candidates?. map (( c ) => ({
1206 namespace: c.ns.namespace,
1207 from: c.ns.from,
1208 reasons: c.reasons,
1209 description: c.description,
1210 site: c.site,
1211 shape: shapeFor (c.record, c.ns.namespace, intent),
1212 record: c.record,
1213 })),
1214 }));
1215 console. log ( JSON . stringify (plain, null , 2 ));
1216 } else {
1217 console. log (results. map (( r ) => render (r, intent, project)). join ( ' \n\n ' ));
1218 }
1219 const codes = results. map (( r ) => ({ found: 0 , unknown: 1 , 'not-installed' : 3 })[r.status]);
1220 return Math. max ( ... codes);
1221 }
1222
1223 if (require.main === module ) process.exitCode = main (process.argv. slice ( 2 ));
1224
1225 module . exports = { findPackages, deriveRecord, lookup, loadCurated };