Setting the file. One moment. Wds · Wix Design System · wix/skills · Skills Docsfunction cmdExample
— line 298
This file
- Number
- 4.2
- Position
- 2 of 2
- Type
- JavaScript
- Size
- 15 KB
- Lines
- 531
scripts/wds.cjs
JavaScript·531 lines·15 KB
16
*/
17
18const fs = require("fs");
19const path = require("path");
20
21// ---------------------------------------------------------------------------
22// Path discovery
23// ---------------------------------------------------------------------------
24
25function tryEnablePnp() {
26 // Yarn Berry PnP projects have no node_modules. Walk up from cwd looking for
27 // .pnp.cjs and activate it so require.resolve can see PnP-managed packages.
28 let dir = process.cwd();
29 while (true) {
30 const pnp = path.join(dir, ".pnp.cjs");
31 if (fs.existsSync(pnp)) {
32 try {
33 require(pnp).setup();
34 } catch {
35 // ignore — fall through to other discovery paths
36 }
37 return;
38 }
39 const parent = path.dirname(dir);
40 if (parent === dir) return;
41 dir = parent;
42 }
43}
44
45function findDocsDir() {
46 tryEnablePnp();
47
48 // Primary: use Node's module resolver (handles symlinks, pnpm, yarn PnP, etc.)
49 try {
50 const pkgPath = require.resolve("@wix/design-system/package.json", {
51 paths: [process.cwd()],
52 });
53 const docsDir = path.join(path.dirname(pkgPath), "dist", "docs");
54 if (fs.existsSync(docsDir)) return docsDir;
55 } catch {
56 // resolve failed — fall through to filesystem scan
57 }
58
59 // Fallback: walk up from cwd looking for node_modules
60 let dir = process.cwd();
61 while (true) {
62 const candidate = path.join(
63 dir,
64 "node_modules",
65 "@wix",
66 "design-system",
67 "dist",
68 "docs"
69 );
70 if (fs.existsSync(candidate)) return candidate;
71
72 const parent = path.dirname(dir);
73 if (parent === dir) break;
74 dir = parent;
75 }
76
77 return null;
78}
79
80function readFile(filePath) {
81 try {
82 return fs.readFileSync(filePath, "utf8");
83 } catch {
84 return null;
85 }
86}
87
88// ---------------------------------------------------------------------------
89// Helpers
90// ---------------------------------------------------------------------------
91
92function escapeRegex(str) {
93 return str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
94}
95
96function buildTermsPattern(args) {
97 // Tolerate both `search foo bar` (separate args) and `search "foo bar"`
98 // (single quoted arg). Without the split, a single quoted string becomes a
99 // literal-phrase match instead of OR-of-words and silently returns nothing.
100 return args
101 .flatMap((a) => a.split(/\s+/))
102 .filter(Boolean)
103 .map(escapeRegex)
104 .join("|");
105}
106
107function validateComponentName(name) {
108 if (!/^[A-Za-z0-9]+$/.test(name)) {
109 console.error(
110 `Invalid component name "${name}". Names may only contain letters and digits.`
111 );
112 process.exit(1);
113 }
114}
115
116// ---------------------------------------------------------------------------
117// Commands
118// ---------------------------------------------------------------------------
119
120function cmdSearch(docsDir, terms) {
121 if (terms.length === 0) {
122 console.error("Usage: wds.cjs search <keyword> [keyword...]");
123 console.error('Example: wds.cjs search form input validation');
124 process.exit(1);
125 }
126
127 const content = readFile(path.join(docsDir, "components.md"));
128 if (!content) {
129 console.error("Error: components.md not found");
130 process.exit(1);
131 }
132
133 const regex = new RegExp(buildTermsPattern(terms), "i");
134 const sections = content.split(/^\s*### /m).slice(1);
135 const matches = [];
136
137 for (const section of sections) {
138 if (regex.test(section)) {
139 const lines = section.split("\n");
140 const name = lines[0].trim();
141 const descLine = lines.find((l) => l.trimStart().startsWith("- description:"));
142 const desc = descLine ? descLine.replace(/.*- description:/, "").trim() : "";
143 const doLine = lines.find((l) => l.trimStart().startsWith("- do:"));
144 const doText = doLine ? doLine.replace(/.*- do:/, "").trim() : "";
145 const dontLine = lines.find((l) => l.trimStart().startsWith("- donts:"));
146 const dontText = dontLine ? dontLine.replace(/.*- donts:/, "").trim() : "";
147 matches.push({ name, description: desc, do: doText, donts: dontText });
148 }
149 }
150
151 if (matches.length === 0) {
152 console.log(`No components found matching "${terms.join(" ")}".`);
153 return;
154 }
155
156 console.log(`Found ${matches.length} component(s):\n`);
157 for (const r of matches) {
158 console.log(`### ${r.name}`);
159 if (r.description) console.log(` ${r.description}`);
160 if (r.do) console.log(` Do: ${r.do}`);
161 if (r.donts) console.log(` Don't: ${r.donts}`);
162 console.log();
163 }
164}
165
166// Renders one component's props + examples list. Returns true on success,
167// false if the component wasn't found (so batch callers can keep going).
168function printComponent(docsDir, componentName) {
169 validateComponentName(componentName);
170
171 const componentsDir = path.join(docsDir, "components");
172
173 // --- Props ---
174 const propsPath = path.join(componentsDir, `${componentName}Props.md`);
175 const propsContent = readFile(propsPath);
176
177 if (!propsContent) {
178 console.error(
179 `Component "${componentName}" not found. Run: wds.cjs search <keyword>`
180 );
181 return false;
182 }
183
184 const propsLines = propsContent.split("\n");
185 const propsLineCount = propsLines.length;
186
187 console.log(`## ${componentName} Props (${propsLineCount} lines)\n`);
188
189 if (propsLineCount <= 200) {
190 // Small file — include full props
191 console.log(propsContent);
192 } else {
193 // Large file — summarize prop names and types only
194 console.log(
195 `(Large props file — showing summary. Use grep for specific prop details.)\n`
196 );
197 for (const line of propsLines) {
198 if (line.startsWith("### ")) {
199 const propName = line.replace("### ", "").trim();
200 // Find the type line (next line starting with "- type:")
201 const idx = propsLines.indexOf(line);
202 const typeLine = propsLines[idx + 1];
203 const type =
204 typeLine && typeLine.startsWith("- type:")
205 ? typeLine.replace("- type:", "").trim()
206 : "";
207 console.log(` ${propName}: ${type}`);
208 }
209 }
210 }
211
212 // --- Examples list ---
213 const examplesPath = path.join(componentsDir, `${componentName}Examples.md`);
214 const examplesContent = readFile(examplesPath);
215
216 if (examplesContent) {
217 const exLines = examplesContent.split("\n");
218 const examples = [];
219 for (let i = 0; i < exLines.length; i++) {
220 if (exLines[i].startsWith("### ")) {
221 examples.push(exLines[i].replace("### ", "").trim());
222 }
223 }
224
225 if (examples.length > 0) {
226 console.log(`\n## Available Examples (${examples.length})\n`);
227 for (const ex of examples) {
228 console.log(` - ${ex}`);
229 }
230 console.log(
231 `\nGet an example: wds.cjs example ${componentName} "<ExampleName>"`
232 );
233 }
234 }
235
236 return true;
237}
238
239function cmdComponent(docsDir, componentName) {
240 if (!componentName) {
241 console.error("Usage: wds.cjs component <ComponentName>");
242 console.error("Example: wds.cjs component Button");
243 process.exit(1);
244 }
245 if (!printComponent(docsDir, componentName)) {
246 process.exit(1);
247 }
248}
249
250// Cheap pre-check so cmdComponents can decide whether to emit a `---`
251// separator before invoking the actual renderer. Avoids stale separators
252// when a middle component is missing.
253function componentExists(docsDir, componentName) {
254 if (!/^[A-Za-z0-9]+$/.test(componentName)) return false;
255 return fs.existsSync(
256 path.join(docsDir, "components", `${componentName}Props.md`)
257 );
258}
259
260function cmdComponents(docsDir, args) {
261 // Accept either `components Button Card` or `components "Button Card"`.
262 const names = args.flatMap((a) => a.split(/\s+/)).filter(Boolean);
263
264 if (names.length === 0) {
265 console.error("Usage: wds.cjs components <Name1> [Name2] [Name3]...");
266 console.error("Example: wds.cjs components Button Card Table");
267 process.exit(1);
268 }
269
270 let printedAny = false;
271 let anyFailed = false;
272 for (const name of names) {
273 // Match the single-component flow: invalid names get a distinct error
274 // instead of being lumped under "not found".
275 if (!/^[A-Za-z0-9]+$/.test(name)) {
276 console.error(
277 `Invalid component name "${name}". Names may only contain letters and digits.`
278 );
279 anyFailed = true;
280 continue;
281 }
282 if (!componentExists(docsDir, name)) {
283 console.error(
284 `Component "${name}" not found. Run: wds.cjs search <keyword>`
285 );
286 anyFailed = true;
287 continue;
288 }
289 if (printedAny) console.log("\n---\n");
290 printComponent(docsDir, name);
291 printedAny = true;
292 }
293
294 // Exit non-zero only if every requested component failed.
295 if (anyFailed && !printedAny) process.exit(1);
296}
297
298function cmdExample(docsDir, componentName, exampleName) {
299 if (!componentName || !exampleName) {
300 console.error('Usage: wds.cjs example <ComponentName> "<ExampleName>"');
301 console.error('Example: wds.cjs example Button "Loading state"');
302 process.exit(1);
303 }
304 validateComponentName(componentName);
305
306 const filePath = path.join(
307 docsDir,
308 "components",
309 `${componentName}Examples.md`
310 );
311 const content = readFile(filePath);
312
313 if (!content) {
314 console.error(`No examples file for "${componentName}".`);
315 process.exit(1);
316 }
317
318 const lines = content.split("\n");
319 let startLine = -1;
320 let endLine = lines.length;
321 const searchName = exampleName.toLowerCase();
322
323 for (let i = 0; i < lines.length; i++) {
324 if (lines[i].startsWith("### ")) {
325 const name = lines[i].replace("### ", "").trim().toLowerCase();
326 if (startLine >= 0) {
327 // Found the next section — stop here
328 endLine = i;
329 break;
330 }
331 if (name === searchName || name.includes(searchName)) {
332 startLine = i;
333 }
334 }
335 }
336
337 if (startLine < 0) {
338 console.error(
339 `Example "${exampleName}" not found for ${componentName}.\n`
340 );
341 // List available examples
342 const available = [];
343 for (const line of lines) {
344 if (line.startsWith("### ")) {
345 available.push(line.replace("### ", "").trim());
346 }
347 }
348 if (available.length > 0) {
349 console.log("Available examples:");
350 for (const ex of available) {
351 console.log(` - ${ex}`);
352 }
353 }
354 process.exit(1);
355 }
356
357 console.log(lines.slice(startLine, endLine).join("\n"));
358}
359
360function cmdTestkit(docsDir, componentName, methodName) {
361 if (!componentName) {
362 console.error("Usage: wds.cjs testkit <ComponentName> [methodName]");
363 console.error("Example: wds.cjs testkit Button");
364 console.error('Example: wds.cjs testkit Button click');
365 process.exit(1);
366 }
367 validateComponentName(componentName);
368
369 const filePath = path.join(
370 docsDir,
371 "components",
372 `${componentName}Testkit.md`,
373 );
374 const content = readFile(filePath);
375
376 if (!content) {
377 console.error(
378 `Testkit docs for "${componentName}" not found. Run: wds.cjs search <keyword>`,
379 );
380 process.exit(1);
381 }
382
383 if (!methodName) {
384 console.log(content);
385 return;
386 }
387
388 const lines = content.split("\n");
389 const searchName = methodName.toLowerCase();
390 let startLine = -1;
391 let endLine = lines.length;
392
393 for (let i = 0; i < lines.length; i++) {
394 if (lines[i].startsWith("### ") && !lines[i].startsWith("### API")) {
395 const name = lines[i].replace("### ", "").trim().toLowerCase();
396 if (startLine >= 0) {
397 endLine = i;
398 break;
399 }
400 if (name === searchName || name.includes(searchName)) {
401 startLine = i;
402 }
403 }
404 }
405
406 if (startLine < 0) {
407 console.error(
408 `Method "${methodName}" not found on ${componentName} testkit.\n`,
409 );
410 const available = [];
411 let inApiSection = false;
412 for (const line of lines) {
413 if (line.startsWith("### API")) {
414 inApiSection = true;
415 continue;
416 }
417 if (inApiSection && line.startsWith("### ")) {
418 available.push(line.replace("### ", "").trim());
419 }
420 }
421 if (available.length > 0) {
422 console.log("Available methods:");
423 for (const m of available) {
424 console.log(` - ${m}`);
425 }
426 }
427 process.exit(1);
428 }
429
430 console.log(lines.slice(startLine, endLine).join("\n"));
431}
432
433function cmdIcons(docsDir, terms) {
434 if (terms.length === 0) {
435 console.error("Usage: wds.cjs icons <query> [query...]");
436 console.error("Example: wds.cjs icons Add Edit Delete");
437 process.exit(1);
438 }
439
440 const content = readFile(path.join(docsDir, "icons.md"));
441 if (!content) {
442 console.error("Error: icons.md not found");
443 process.exit(1);
444 }
445
446 const regex = new RegExp(buildTermsPattern(terms), "i");
447 const matches = [];
448
449 for (const line of content.split("\n")) {
450 if (line.trim() && regex.test(line)) {
451 matches.push(line.trim());
452 }
453 }
454
455 if (matches.length === 0) {
456 console.log(`No icons found matching "${terms.join(" ")}".`);
457 return;
458 }
459
460 console.log(`Found ${matches.length} icon(s):\n`);
461 for (const m of matches) {
462 console.log(` ${m}`);
463 }
464 console.log(
465 "\nIcons are from @wix/wix-ui-icons-common. Each icon has a Small variant (e.g., Add + AddSmall)."
466 );
467}
468
469function cmdHelp(docsDir) {
470 const scriptPath = path.resolve(__dirname, "wds.cjs");
471 console.log(`WDS Documentation Helper
472
473Usage:
474 node ${scriptPath} search <keyword> Search components by keyword
475 node ${scriptPath} component <Name> Get props + example list
476 node ${scriptPath} components <Name1> <Name2>... Get props + example list for multiple components in one call
477 node ${scriptPath} example <Name> "<ExampleName>" Get a specific example
478 node ${scriptPath} testkit <Name> [method] Get testkit imports + API (or one method)
479 node ${scriptPath} icons <query> Search for icons
480
481Examples:
482 node ${scriptPath} search table list
483 node ${scriptPath} search form input validation
484 node ${scriptPath} component Button
485 node ${scriptPath} components Button Card Table Input
486 node ${scriptPath} example Button "Loading state"
487 node ${scriptPath} testkit Button
488 node ${scriptPath} testkit Button click
489 node ${scriptPath} icons Add Edit Delete
490
491Docs found at: ${docsDir}`);
492}
493
494// ---------------------------------------------------------------------------
495// Main
496// ---------------------------------------------------------------------------
497
498const docsDir = findDocsDir();
499if (!docsDir) {
500 console.error(
501 "Error: @wix/design-system not found in node_modules.\n" +
502 "Install it first: npm i @wix/design-system"
503 );
504 process.exit(1);
505}
506
507const [command, ...args] = process.argv.slice(2);
508
509switch (command) {
510 case "search":
511 cmdSearch(docsDir, args);
512 break;
513 case "component":
514 cmdComponent(docsDir, args[0]);
515 break;
516 case "components":
517 cmdComponents(docsDir, args);
518 break;
519 case "example":
520 cmdExample(docsDir, args[0], args.slice(1).join(" "));
521 break;
522 case "testkit":
523 cmdTestkit(docsDir, args[0], args[1]);
524 break;
525 case "icons":
526 cmdIcons(docsDir, args);
527 break;
528 default:
529 cmdHelp(docsDir);
530 break;
531}