@wix/agent-skills 1.16.0 → 1.16.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +72 -8
  3. package/skills/wix-app/references/CODE_QUALITY.md +0 -1
  4. package/skills/wix-app/references/DASHBOARD_MODAL.md +21 -14
  5. package/skills/wix-app/references/DASHBOARD_PAGE.md +30 -184
  6. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +19 -3
  7. package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +172 -0
  8. package/skills/wix-app/references/dashboard-page/WDS_LAYOUT.md +187 -0
  9. package/skills/wix-app/references/editor-react-component/A11Y-REVIEW.md +12 -7
  10. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +31 -2
  11. package/skills/wix-app/references/editor-react-component/COMPONENT-API.md +6 -1
  12. package/skills/wix-app/references/editor-react-component/COMPONENT-CONFIGURATION.md +60 -0
  13. package/skills/wix-app/references/editor-react-component/DIRECTIONALITY.md +6 -6
  14. package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +167 -0
  15. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +2 -2
  16. package/skills/wix-app/references/editor-react-component/SITE-CONTEXT-HOOKS.md +187 -0
  17. package/skills/wix-app/scripts/scan-a11y-code.cjs +777 -0
  18. package/skills/wix-app/scripts/scan-a11y-eslint.cjs +201 -0
  19. package/skills/wix-base44-connector/SKILL.md +185 -0
  20. package/skills/wix-base44-connector/scripts/utils.js +212 -0
  21. package/skills/wix-design-system/SKILL.md +19 -15
  22. package/skills/wix-docs/SKILL.md +75 -1
  23. package/skills/wix-headless/references/CAPABILITIES.md +16 -3
  24. package/skills/wix-headless/references/SDK_HANDOFF.md +1 -1
  25. package/skills/wix-headless/references/SEED.md +1 -1
  26. package/skills/wix-headless/references/inline-recipes/how-to-code-forms.md +560 -102
  27. package/skills/wix-headless/references/inline-recipes/setup-forms.md +210 -143
  28. package/skills/wix-manage/SKILL.md +20 -17
  29. package/skills/wix-manage/references/accessibility/scan-site-accessibility.md +98 -0
  30. package/skills/wix-manage/references/analytics/analytics-dashboard-navigation.md +1 -2
  31. package/skills/wix-manage/references/domains/domain-search-purchase-and-connect.md +2603 -0
  32. package/skills/wix-manage/references/events/create-wix-event.md +112 -90
  33. package/skills/wix-manage/references/events/manage-wix-events.md +33 -94
  34. package/skills/wix-manage/references/sites/create-headless-site.md +1 -0
  35. package/skills/wix-manage/references/sites/manage-oauth-apps.md +154 -0
  36. package/skills/wix-manage/references/sites/read-site-context.md +125 -0
  37. package/skills/wix-manage/references/sites/site-import.md +67 -15
  38. package/skills/wix-manage/references/stores/create-product-catalog-v3.md +273 -0
  39. package/skills/wix-replatform/resources/rp-target-wix/lib/wix-writers.js +1 -1
  40. package/skills/wix-vibe-headless/SKILL.md +2 -1
  41. package/skills/wix-vibe-headless/install/deploy.cjs +63 -3
  42. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +104 -0
  43. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +74 -0
  44. package/skills/wix-vibe-headless/platforms/base44.md +15 -40
  45. package/skills/wix-vibe-headless/references/blog/INSTRUCTIONS.md +7 -11
  46. package/skills/wix-vibe-headless/references/blog/seed/SEED.md +1 -1
  47. package/skills/wix-vibe-headless/references/bookings/INSTRUCTIONS.md +7 -11
  48. package/skills/wix-vibe-headless/references/bookings/seed/SEED.md +1 -1
  49. package/skills/wix-vibe-headless/references/cms/INSTRUCTIONS.md +1 -2
  50. package/skills/wix-vibe-headless/references/events/INSTRUCTIONS.md +7 -12
  51. package/skills/wix-vibe-headless/references/events/seed/SEED.md +1 -1
  52. package/skills/wix-vibe-headless/references/members/INSTRUCTIONS.md +7 -11
  53. package/skills/wix-vibe-headless/references/portfolio/INSTRUCTIONS.md +7 -11
  54. package/skills/wix-vibe-headless/references/portfolio/seed/SEED.md +1 -1
  55. package/skills/wix-vibe-headless/references/pricing-plans/INSTRUCTIONS.md +7 -11
  56. package/skills/wix-vibe-headless/references/pricing-plans/seed/SEED.md +1 -1
  57. package/skills/wix-vibe-headless/references/restaurants/INSTRUCTIONS.md +7 -11
  58. package/skills/wix-vibe-headless/references/restaurants/seed/SEED.md +1 -1
  59. package/skills/wix-vibe-headless/references/shared/app/components/WixManageBanner.jsx +1 -1
  60. package/skills/wix-vibe-headless/references/shared/app/rest/wix-config.js +5 -3
  61. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +75 -13
  62. package/skills/wix-vibe-headless/references/storefront/app/components/ProductCard.jsx +29 -86
  63. package/skills/wix-vibe-headless/references/storefront/app/components/ProductGrid.jsx +3 -8
  64. package/skills/wix-vibe-headless/references/storefront/app/components/VariantPicker.jsx +78 -105
  65. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +92 -0
  66. package/skills/wix-vibe-headless/references/storefront/app/hooks/useVariantOptions.js +76 -0
  67. package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +1 -1
  68. package/skills/wix-vibe-headless/references/storefront/seed/seed-store.js +44 -5
  69. package/skills/wix-app/references/AUTO_PATTERNS_DASHBOARD.md +0 -326
  70. package/skills/wix-app/references/auto-patterns-dashboard/action-cell.md +0 -115
  71. package/skills/wix-app/references/auto-patterns-dashboard/app-config-structure.md +0 -264
  72. package/skills/wix-app/references/auto-patterns-dashboard/app-context.md +0 -37
  73. package/skills/wix-app/references/auto-patterns-dashboard/bulk-actions.md +0 -86
  74. package/skills/wix-app/references/auto-patterns-dashboard/collection-page-actions.md +0 -119
  75. package/skills/wix-app/references/auto-patterns-dashboard/collection-page.md +0 -103
  76. package/skills/wix-app/references/auto-patterns-dashboard/custom-actions-override.md +0 -76
  77. package/skills/wix-app/references/auto-patterns-dashboard/custom-columns-override.md +0 -82
  78. package/skills/wix-app/references/auto-patterns-dashboard/custom-components-override.md +0 -89
  79. package/skills/wix-app/references/auto-patterns-dashboard/custom-header-override.md +0 -124
  80. package/skills/wix-app/references/auto-patterns-dashboard/custom-sections-override.md +0 -91
  81. package/skills/wix-app/references/auto-patterns-dashboard/custom-slots-override.md +0 -71
  82. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-actions.md +0 -70
  83. package/skills/wix-app/references/auto-patterns-dashboard/entity-page-view-actions.md +0 -73
  84. package/skills/wix-app/references/auto-patterns-dashboard/entity-page.md +0 -98
  85. package/skills/wix-app/references/auto-patterns-dashboard/example-patterns.json +0 -278
  86. package/skills/wix-app/references/auto-patterns-dashboard/pages-configuration.md +0 -94
  87. package/skills/wix-app/references/auto-patterns-dashboard/resolved-action.md +0 -43
  88. package/skills/wix-app/references/auto-patterns-dashboard/sdk-utilities.md +0 -66
  89. package/skills/wix-app/references/auto-patterns-dashboard/views.md +0 -76
  90. package/skills/wix-app/scripts/generate-auto-patterns.js +0 -510
  91. package/skills/wix-manage/references/domains/domain-search-and-purchase.md +0 -437
  92. package/skills/wix-manage/references/stores/create-product-from-image.md +0 -1031
  93. package/skills/wix-manage/references/stores/create-product-with-options-catalog-v3.md +0 -623
  94. package/skills/wix-manage/references/stores/setup-online-store-catalog-v3.md +0 -106
@@ -0,0 +1,777 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // oxlint-disable no-console no-shadow preserve-caught-error
5
+
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { createRequire } = require('module');
9
+
10
+ const ROOT = process.cwd();
11
+ const LOCAL_REQUIRE = createRequire(__filename);
12
+ const ROOT_REQUIRE = createRequire(path.join(ROOT, 'package.json'));
13
+ const parser = loadModule('@babel/parser');
14
+ const traverse = loadModule('@babel/traverse').default;
15
+ const t = loadModule('@babel/types');
16
+
17
+ const MAX_RESOLUTION_DEPTH = 4;
18
+ const VALID_ARIA_PROPS = new Set([
19
+ 'aria-activedescendant',
20
+ 'aria-atomic',
21
+ 'aria-autocomplete',
22
+ 'aria-braillelabel',
23
+ 'aria-brailleroledescription',
24
+ 'aria-busy',
25
+ 'aria-checked',
26
+ 'aria-colcount',
27
+ 'aria-colindex',
28
+ 'aria-colindextext',
29
+ 'aria-colspan',
30
+ 'aria-controls',
31
+ 'aria-current',
32
+ 'aria-describedby',
33
+ 'aria-description',
34
+ 'aria-details',
35
+ 'aria-disabled',
36
+ 'aria-dropeffect',
37
+ 'aria-errormessage',
38
+ 'aria-expanded',
39
+ 'aria-flowto',
40
+ 'aria-grabbed',
41
+ 'aria-haspopup',
42
+ 'aria-hidden',
43
+ 'aria-invalid',
44
+ 'aria-keyshortcuts',
45
+ 'aria-label',
46
+ 'aria-labelledby',
47
+ 'aria-level',
48
+ 'aria-live',
49
+ 'aria-modal',
50
+ 'aria-multiline',
51
+ 'aria-multiselectable',
52
+ 'aria-orientation',
53
+ 'aria-owns',
54
+ 'aria-placeholder',
55
+ 'aria-posinset',
56
+ 'aria-pressed',
57
+ 'aria-readonly',
58
+ 'aria-relevant',
59
+ 'aria-required',
60
+ 'aria-roledescription',
61
+ 'aria-rowcount',
62
+ 'aria-rowindex',
63
+ 'aria-rowindextext',
64
+ 'aria-rowspan',
65
+ 'aria-selected',
66
+ 'aria-setsize',
67
+ 'aria-sort',
68
+ 'aria-valuemax',
69
+ 'aria-valuemin',
70
+ 'aria-valuenow',
71
+ 'aria-valuetext',
72
+ ]);
73
+ const VALID_ROLES = new Set([
74
+ 'alert',
75
+ 'alertdialog',
76
+ 'application',
77
+ 'article',
78
+ 'banner',
79
+ 'button',
80
+ 'cell',
81
+ 'checkbox',
82
+ 'columnheader',
83
+ 'combobox',
84
+ 'complementary',
85
+ 'contentinfo',
86
+ 'definition',
87
+ 'dialog',
88
+ 'directory',
89
+ 'document',
90
+ 'feed',
91
+ 'figure',
92
+ 'form',
93
+ 'grid',
94
+ 'gridcell',
95
+ 'group',
96
+ 'heading',
97
+ 'img',
98
+ 'link',
99
+ 'list',
100
+ 'listbox',
101
+ 'listitem',
102
+ 'log',
103
+ 'main',
104
+ 'marquee',
105
+ 'math',
106
+ 'menu',
107
+ 'menubar',
108
+ 'menuitem',
109
+ 'menuitemcheckbox',
110
+ 'menuitemradio',
111
+ 'navigation',
112
+ 'none',
113
+ 'note',
114
+ 'option',
115
+ 'presentation',
116
+ 'progressbar',
117
+ 'radio',
118
+ 'radiogroup',
119
+ 'region',
120
+ 'row',
121
+ 'rowgroup',
122
+ 'rowheader',
123
+ 'scrollbar',
124
+ 'search',
125
+ 'searchbox',
126
+ 'separator',
127
+ 'slider',
128
+ 'spinbutton',
129
+ 'status',
130
+ 'switch',
131
+ 'tab',
132
+ 'table',
133
+ 'tablist',
134
+ 'tabpanel',
135
+ 'term',
136
+ 'textbox',
137
+ 'timer',
138
+ 'toolbar',
139
+ 'tooltip',
140
+ 'tree',
141
+ 'treegrid',
142
+ 'treeitem',
143
+ ]);
144
+ const UNSUPPORTED_ARIA_ELEMENTS = new Set([
145
+ 'meta',
146
+ 'script',
147
+ 'style',
148
+ 'head',
149
+ 'html',
150
+ 'base',
151
+ 'link',
152
+ 'param',
153
+ 'source',
154
+ 'track',
155
+ 'col',
156
+ 'colgroup',
157
+ ]);
158
+
159
+ const FILE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs'];
160
+ const parseCache = new Map();
161
+ const resolutionWarnings = [];
162
+
163
+ function loadModule(name) {
164
+ try {
165
+ return LOCAL_REQUIRE(name);
166
+ } catch (localError) {
167
+ try {
168
+ return ROOT_REQUIRE(name);
169
+ } catch (rootError) {
170
+ throw new Error(
171
+ `Missing dependency "${name}". Local resolution failed: ${localError.message}. Root resolution failed: ${rootError.message}`,
172
+ );
173
+ }
174
+ }
175
+ }
176
+
177
+ function parseFile(filePath) {
178
+ if (parseCache.has(filePath)) return parseCache.get(filePath);
179
+
180
+ try {
181
+ const code = fs.readFileSync(filePath, 'utf8');
182
+ const ast = parser.parse(code, {
183
+ sourceType: 'unambiguous',
184
+ plugins: [
185
+ 'jsx',
186
+ 'typescript',
187
+ 'classProperties',
188
+ 'objectRestSpread',
189
+ 'optionalChaining',
190
+ 'nullishCoalescingOperator',
191
+ ],
192
+ errorRecovery: true,
193
+ });
194
+ const recoverableErrors = Array.isArray(ast.errors)
195
+ ? ast.errors.map((error) => error.message)
196
+ : [];
197
+ const parsed = { ok: true, ast, code, recoverableErrors };
198
+ parseCache.set(filePath, parsed);
199
+ return parsed;
200
+ } catch (error) {
201
+ const parsed = { ok: false, error };
202
+ parseCache.set(filePath, parsed);
203
+ return parsed;
204
+ }
205
+ }
206
+
207
+ function getJsxName(node) {
208
+ if (t.isJSXIdentifier(node)) return node.name;
209
+ if (t.isJSXMemberExpression(node))
210
+ return `${getJsxName(node.object)}.${getJsxName(node.property)}`;
211
+ if (t.isJSXNamespacedName(node)) return `${node.namespace.name}:${node.name.name}`;
212
+ return null;
213
+ }
214
+
215
+ function getAttribute(node, name) {
216
+ return (
217
+ node.attributes.find((attr) => t.isJSXAttribute(attr) && getJsxName(attr.name) === name) || null
218
+ );
219
+ }
220
+
221
+ function getLiteralAttributeValue(attr) {
222
+ if (!attr) return undefined;
223
+ if (!attr.value) return true;
224
+ if (t.isStringLiteral(attr.value)) return attr.value.value;
225
+ if (t.isJSXExpressionContainer(attr.value)) {
226
+ const expr = attr.value.expression;
227
+ if (t.isStringLiteral(expr)) return expr.value;
228
+ if (t.isBooleanLiteral(expr)) return expr.value;
229
+ if (t.isNumericLiteral(expr)) return expr.value;
230
+ if (t.isTemplateLiteral(expr) && expr.expressions.length === 0) {
231
+ return expr.quasis.map((q) => q.value.cooked || '').join('');
232
+ }
233
+ }
234
+ return undefined;
235
+ }
236
+
237
+ function hasTruthyAttribute(node, name) {
238
+ const attr = getAttribute(node, name);
239
+ return Boolean(attr);
240
+ }
241
+
242
+ function isNativeTag(name) {
243
+ return Boolean(name && /^[a-z]/.test(name));
244
+ }
245
+
246
+ function getImportMap(ast) {
247
+ const imports = new Map();
248
+
249
+ traverse(ast, {
250
+ ImportDeclaration(path) {
251
+ const source = path.node.source.value;
252
+ for (const specifier of path.node.specifiers) {
253
+ if (t.isImportDefaultSpecifier(specifier)) {
254
+ imports.set(specifier.local.name, { source, imported: 'default' });
255
+ } else if (t.isImportSpecifier(specifier)) {
256
+ imports.set(specifier.local.name, { source, imported: specifier.imported.name });
257
+ } else if (t.isImportNamespaceSpecifier(specifier)) {
258
+ imports.set(specifier.local.name, { source, imported: '*' });
259
+ }
260
+ }
261
+ },
262
+ });
263
+
264
+ return imports;
265
+ }
266
+
267
+ function findRootJsx(pathLike) {
268
+ if (!pathLike) return null;
269
+ if (
270
+ pathLike.isFunctionDeclaration() ||
271
+ pathLike.isFunctionExpression() ||
272
+ pathLike.isArrowFunctionExpression()
273
+ ) {
274
+ if (t.isJSXElement(pathLike.node.body) || t.isJSXFragment(pathLike.node.body))
275
+ return pathLike.node.body;
276
+ if (!t.isBlockStatement(pathLike.node.body)) return null;
277
+
278
+ for (const statement of pathLike.node.body.body) {
279
+ if (!t.isReturnStatement(statement)) continue;
280
+ const arg = statement.argument;
281
+ if (t.isJSXElement(arg) || t.isJSXFragment(arg)) return arg;
282
+ }
283
+ }
284
+ return null;
285
+ }
286
+
287
+ function resolveSourceFile(fromFile, source) {
288
+ const basedir = path.dirname(fromFile);
289
+ const tryFile = (candidate) => {
290
+ if (candidate && fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
291
+ return null;
292
+ };
293
+
294
+ if (source.startsWith('.')) {
295
+ const base = path.resolve(basedir, source);
296
+ for (const ext of FILE_EXTENSIONS) {
297
+ const direct = tryFile(base + ext);
298
+ if (direct) return direct;
299
+ }
300
+ for (const ext of FILE_EXTENSIONS) {
301
+ const indexFile = tryFile(path.join(base, `index${ext}`));
302
+ if (indexFile) return indexFile;
303
+ }
304
+ return tryFile(base);
305
+ }
306
+
307
+ try {
308
+ return require.resolve(source, { paths: [basedir, ROOT] });
309
+ } catch (error) {
310
+ resolutionWarnings.push({
311
+ source,
312
+ fromFile,
313
+ method: 'require.resolve',
314
+ message: error.message,
315
+ });
316
+ }
317
+
318
+ try {
319
+ return ROOT_REQUIRE.resolve(source);
320
+ } catch (error) {
321
+ resolutionWarnings.push({
322
+ source,
323
+ fromFile,
324
+ method: 'rootRequire.resolve',
325
+ message: error.message,
326
+ });
327
+ }
328
+
329
+ return null;
330
+ }
331
+
332
+ function findExportedComponent(ast, exportName) {
333
+ let found = null;
334
+
335
+ traverse(ast, {
336
+ FunctionDeclaration(path) {
337
+ if (found) return;
338
+ if (path.node.id && path.node.id.name === exportName) found = findRootJsx(path);
339
+ },
340
+ VariableDeclarator(path) {
341
+ if (found) return;
342
+ if (!t.isIdentifier(path.node.id, { name: exportName })) return;
343
+ const initPath = path.get('init');
344
+ found = findRootJsx(initPath);
345
+ },
346
+ ExportDefaultDeclaration(path) {
347
+ if (found || exportName !== 'default') return;
348
+ const declPath = path.get('declaration');
349
+ if (declPath.isIdentifier()) {
350
+ found = findNamedBindingJsx(ast, declPath.node.name);
351
+ } else {
352
+ found = findRootJsx(declPath);
353
+ }
354
+ },
355
+ });
356
+
357
+ return found;
358
+ }
359
+
360
+ function findNamedBindingJsx(ast, name) {
361
+ let found = null;
362
+ traverse(ast, {
363
+ FunctionDeclaration(path) {
364
+ if (found) return;
365
+ if (path.node.id && path.node.id.name === name) found = findRootJsx(path);
366
+ },
367
+ VariableDeclarator(path) {
368
+ if (found) return;
369
+ if (!t.isIdentifier(path.node.id, { name })) return;
370
+ found = findRootJsx(path.get('init'));
371
+ },
372
+ });
373
+ return found;
374
+ }
375
+
376
+ function inferByName(name) {
377
+ const lower = String(name || '').toLowerCase();
378
+ if (!lower) return null;
379
+ if (
380
+ /(^|\.)(img|image|avatar|thumbnail|photo|picture)$/.test(lower) ||
381
+ /(image|avatar|thumbnail|photo|picture)/.test(lower)
382
+ ) {
383
+ return {
384
+ semanticType: 'img',
385
+ confidence: 'low',
386
+ sourceKind: 'heuristic',
387
+ evidence: `Component name "${name}" looks image-like.`,
388
+ };
389
+ }
390
+ if (/(^|\.)(link|anchor|navlink)$/.test(lower) || /(link|anchor)/.test(lower)) {
391
+ return {
392
+ semanticType: 'a',
393
+ confidence: 'low',
394
+ sourceKind: 'heuristic',
395
+ evidence: `Component name "${name}" looks link-like.`,
396
+ };
397
+ }
398
+ if (/(button|btn|iconbutton|textbutton|closebutton)/.test(lower)) {
399
+ return {
400
+ semanticType: 'button',
401
+ confidence: 'low',
402
+ sourceKind: 'heuristic',
403
+ evidence: `Component name "${name}" looks button-like.`,
404
+ };
405
+ }
406
+ if (/(textarea|editor)/.test(lower)) {
407
+ return {
408
+ semanticType: 'textarea',
409
+ confidence: 'low',
410
+ sourceKind: 'heuristic',
411
+ evidence: `Component name "${name}" looks textarea-like.`,
412
+ };
413
+ }
414
+ if (/(input|textfield|search|select|checkbox|radio|switch|toggle)/.test(lower)) {
415
+ return {
416
+ semanticType: 'input',
417
+ confidence: 'low',
418
+ sourceKind: 'heuristic',
419
+ evidence: `Component name "${name}" looks input-like.`,
420
+ };
421
+ }
422
+ return null;
423
+ }
424
+
425
+ function inferFromProps(openingElement) {
426
+ const asValue = getLiteralAttributeValue(getAttribute(openingElement, 'as'));
427
+ if (typeof asValue === 'string' && isNativeTag(asValue)) {
428
+ return {
429
+ semanticType: asValue,
430
+ confidence: 'high',
431
+ sourceKind: 'polymorphic-prop',
432
+ evidence: `The component explicitly sets as="${asValue}".`,
433
+ };
434
+ }
435
+
436
+ const componentValue = getLiteralAttributeValue(getAttribute(openingElement, 'component'));
437
+ if (typeof componentValue === 'string' && isNativeTag(componentValue)) {
438
+ return {
439
+ semanticType: componentValue,
440
+ confidence: 'high',
441
+ sourceKind: 'polymorphic-prop',
442
+ evidence: `The component explicitly sets component="${componentValue}".`,
443
+ };
444
+ }
445
+
446
+ if (hasTruthyAttribute(openingElement, 'src')) {
447
+ return {
448
+ semanticType: 'img',
449
+ confidence: hasTruthyAttribute(openingElement, 'alt') ? 'low' : 'medium',
450
+ sourceKind: 'prop-evidence',
451
+ evidence: 'The component receives src-related props that suggest image semantics.',
452
+ };
453
+ }
454
+
455
+ if (hasTruthyAttribute(openingElement, 'href') || hasTruthyAttribute(openingElement, 'to')) {
456
+ return {
457
+ semanticType: 'a',
458
+ confidence: 'medium',
459
+ sourceKind: 'prop-evidence',
460
+ evidence: 'The component receives href/to props that suggest link semantics.',
461
+ };
462
+ }
463
+
464
+ if (hasTruthyAttribute(openingElement, 'onClick')) {
465
+ return {
466
+ semanticType: 'button',
467
+ confidence: 'low',
468
+ sourceKind: 'prop-evidence',
469
+ evidence: 'The component receives onClick, which may indicate button-like behavior.',
470
+ };
471
+ }
472
+
473
+ return null;
474
+ }
475
+
476
+ function chooseResolution(current, next) {
477
+ if (!next) return current;
478
+ if (!current) return next;
479
+ const rank = { high: 3, medium: 2, low: 1, unknown: 0 };
480
+ return rank[next.confidence] > rank[current.confidence] ? next : current;
481
+ }
482
+
483
+ function resolveComponentSemantic(filePath, openingElement, context, depth = 0) {
484
+ const name = getJsxName(openingElement.name);
485
+ if (!name)
486
+ return {
487
+ semanticType: 'unknown',
488
+ confidence: 'unknown',
489
+ sourceKind: 'unknown',
490
+ evidence: 'Unable to resolve JSX element name.',
491
+ };
492
+
493
+ if (isNativeTag(name)) {
494
+ return {
495
+ semanticType: name,
496
+ confidence: 'high',
497
+ sourceKind: 'native',
498
+ evidence: `The JSX element is the native tag <${name}>.`,
499
+ };
500
+ }
501
+
502
+ let resolved = chooseResolution(null, inferFromProps(openingElement));
503
+ resolved = chooseResolution(resolved, inferByName(name));
504
+
505
+ if (depth >= MAX_RESOLUTION_DEPTH) {
506
+ return (
507
+ resolved || {
508
+ semanticType: 'unknown',
509
+ confidence: 'unknown',
510
+ sourceKind: 'unknown',
511
+ evidence: `Resolution depth exceeded for ${name}.`,
512
+ }
513
+ );
514
+ }
515
+
516
+ const importInfo = context.imports.get(name);
517
+ if (importInfo) {
518
+ const resolvedSource = resolveSourceFile(filePath, importInfo.source);
519
+ if (resolvedSource) {
520
+ const parsed = parseFile(resolvedSource);
521
+ if (parsed.ok) {
522
+ const rootJsx = findExportedComponent(parsed.ast, importInfo.imported);
523
+ if (rootJsx && t.isJSXElement(rootJsx)) {
524
+ const innerImports = getImportMap(parsed.ast);
525
+ const nested = resolveComponentSemantic(
526
+ resolvedSource,
527
+ rootJsx.openingElement,
528
+ { imports: innerImports },
529
+ depth + 1,
530
+ );
531
+ if (nested.semanticType !== 'unknown') {
532
+ const sourceKind = importInfo.source.startsWith('.')
533
+ ? 'local-wrapper'
534
+ : 'package-component';
535
+ resolved = chooseResolution(resolved, {
536
+ semanticType: nested.semanticType,
537
+ confidence: nested.confidence === 'low' ? 'medium' : nested.confidence,
538
+ sourceKind,
539
+ evidence: `${name} resolves through ${path.relative(ROOT, resolvedSource)} to ${nested.semanticType} semantics.`,
540
+ });
541
+ }
542
+ }
543
+ }
544
+ }
545
+ }
546
+
547
+ const finalResolution = resolved || {
548
+ semanticType: 'unknown',
549
+ confidence: 'unknown',
550
+ sourceKind: 'unknown',
551
+ evidence: `Could not infer reliable semantics for ${name}.`,
552
+ };
553
+ return finalResolution;
554
+ }
555
+
556
+ function toRelative(filePath) {
557
+ return path.relative(ROOT, filePath) || filePath;
558
+ }
559
+
560
+ function findingFromNode(filePath, node, data) {
561
+ return {
562
+ file: toRelative(filePath),
563
+ line: node.loc ? node.loc.start.line : null,
564
+ column: node.loc ? node.loc.start.column + 1 : null,
565
+ rule: data.rule,
566
+ confidence: data.confidence,
567
+ message: data.message,
568
+ componentName: data.componentName,
569
+ semanticType: data.semanticType,
570
+ evidence: data.evidence,
571
+ sourceKind: data.sourceKind,
572
+ };
573
+ }
574
+
575
+ function scanFile(filePath) {
576
+ const parsed = parseFile(filePath);
577
+ if (!parsed.ok) {
578
+ return {
579
+ parseError: {
580
+ file: toRelative(filePath),
581
+ message: parsed.error.message,
582
+ },
583
+ findings: [],
584
+ };
585
+ }
586
+
587
+ const imports = getImportMap(parsed.ast);
588
+ const findings = [];
589
+ const parseWarnings = parsed.recoverableErrors || [];
590
+
591
+ traverse(parsed.ast, {
592
+ JSXOpeningElement(path) {
593
+ const node = path.node;
594
+ const name = getJsxName(node.name);
595
+ if (!name) return;
596
+
597
+ const semantic = resolveComponentSemantic(filePath, node, { imports });
598
+ const ariaAttrs = node.attributes.filter(
599
+ (attr) => t.isJSXAttribute(attr) && getJsxName(attr.name)?.startsWith('aria-'),
600
+ );
601
+ const roleAttr = getAttribute(node, 'role');
602
+
603
+ if (semantic.semanticType === 'img' && semantic.confidence !== 'low') {
604
+ const altAttr = getAttribute(node, 'alt');
605
+ if (!altAttr) {
606
+ findings.push(
607
+ findingFromNode(filePath, node, {
608
+ rule: 'alt-text',
609
+ confidence: semantic.confidence,
610
+ message: `${name} is treated as image-like but is missing an alt prop.`,
611
+ componentName: name,
612
+ semanticType: semantic.semanticType,
613
+ evidence: semantic.evidence,
614
+ sourceKind: semantic.sourceKind,
615
+ }),
616
+ );
617
+ }
618
+ } else if (semantic.semanticType === 'img' && semantic.confidence === 'low') {
619
+ const altAttr = getAttribute(node, 'alt');
620
+ if (!altAttr) {
621
+ findings.push(
622
+ findingFromNode(filePath, node, {
623
+ rule: 'alt-text',
624
+ confidence: semantic.confidence,
625
+ message: `${name} may be image-like and appears to be missing an alt prop.`,
626
+ componentName: name,
627
+ semanticType: semantic.semanticType,
628
+ evidence: semantic.evidence,
629
+ sourceKind: semantic.sourceKind,
630
+ }),
631
+ );
632
+ }
633
+ }
634
+
635
+ if (semantic.semanticType === 'a') {
636
+ const hrefAttr = getAttribute(node, 'href');
637
+ const toAttr = getAttribute(node, 'to');
638
+ const hrefValue = getLiteralAttributeValue(hrefAttr);
639
+ const toValue = getLiteralAttributeValue(toAttr);
640
+ const invalidLinkTarget =
641
+ (!hrefAttr && !toAttr) ||
642
+ hrefValue === '' ||
643
+ hrefValue === '#' ||
644
+ hrefValue === 'javascript:void(0)' ||
645
+ toValue === '' ||
646
+ toValue === '#' ||
647
+ toValue === 'javascript:void(0)';
648
+ if (invalidLinkTarget) {
649
+ findings.push(
650
+ findingFromNode(filePath, node, {
651
+ rule: 'anchor-is-valid',
652
+ confidence: semantic.confidence,
653
+ message:
654
+ semantic.confidence === 'low'
655
+ ? `${name} may be link-like but does not appear to provide a valid navigation target.`
656
+ : `${name} is treated as link-like but does not provide a valid navigation target.`,
657
+ componentName: name,
658
+ semanticType: semantic.semanticType,
659
+ evidence: semantic.evidence,
660
+ sourceKind: semantic.sourceKind,
661
+ }),
662
+ );
663
+ }
664
+ }
665
+
666
+ for (const attr of ariaAttrs) {
667
+ const attrName = getJsxName(attr.name);
668
+ if (!VALID_ARIA_PROPS.has(attrName)) {
669
+ findings.push(
670
+ findingFromNode(filePath, attr, {
671
+ rule: 'aria-props',
672
+ confidence: 'high',
673
+ message: `${attrName} is not a valid ARIA attribute name.`,
674
+ componentName: name,
675
+ semanticType: semantic.semanticType,
676
+ evidence: `The attribute name "${attrName}" is not in the allowed ARIA prop set.`,
677
+ sourceKind: semantic.sourceKind,
678
+ }),
679
+ );
680
+ }
681
+ }
682
+
683
+ const roleValue = getLiteralAttributeValue(roleAttr);
684
+ if (typeof roleValue === 'string' && !VALID_ROLES.has(roleValue)) {
685
+ findings.push(
686
+ findingFromNode(filePath, roleAttr, {
687
+ rule: 'aria-role',
688
+ confidence: 'high',
689
+ message: `"${roleValue}" is not a valid ARIA role value.`,
690
+ componentName: name,
691
+ semanticType: semantic.semanticType,
692
+ evidence: `The role value "${roleValue}" is not in the supported ARIA roles set.`,
693
+ sourceKind: semantic.sourceKind,
694
+ }),
695
+ );
696
+ }
697
+
698
+ if ((ariaAttrs.length > 0 || roleAttr) && semantic.confidence !== 'low') {
699
+ const semanticTag = semantic.semanticType;
700
+ if (UNSUPPORTED_ARIA_ELEMENTS.has(semanticTag)) {
701
+ findings.push(
702
+ findingFromNode(filePath, node, {
703
+ rule: 'aria-unsupported-elements',
704
+ confidence: semantic.confidence,
705
+ message: `${name} resolves to unsupported element <${semanticTag}> but carries ARIA attributes or role.`,
706
+ componentName: name,
707
+ semanticType: semantic.semanticType,
708
+ evidence: semantic.evidence,
709
+ sourceKind: semantic.sourceKind,
710
+ }),
711
+ );
712
+ }
713
+ }
714
+ },
715
+ });
716
+
717
+ return { parseError: null, findings, parseWarnings };
718
+ }
719
+
720
+ function main() {
721
+ const files = process.argv.slice(2);
722
+ if (files.length === 0) {
723
+ console.log(
724
+ JSON.stringify(
725
+ {
726
+ error: 'No files specified.',
727
+ usage: 'node <SKILL_ROOT>/scripts/scan-a11y-code.cjs <file1> [file2] ...',
728
+ },
729
+ null,
730
+ 2,
731
+ ),
732
+ );
733
+ process.exit(1);
734
+ }
735
+
736
+ const meta = {
737
+ filesScanned: files.length,
738
+ parser: '@babel/parser',
739
+ supportedRules: [
740
+ 'alt-text',
741
+ 'anchor-is-valid',
742
+ 'aria-props',
743
+ 'aria-role',
744
+ 'aria-unsupported-elements',
745
+ ],
746
+ confidenceModel: ['high', 'medium', 'low', 'unknown'],
747
+ parseErrors: [],
748
+ resolutionWarnings,
749
+ };
750
+
751
+ const findings = [];
752
+
753
+ for (const file of files.map((file) => path.resolve(file))) {
754
+ const result = scanFile(file);
755
+ if (result.parseError) meta.parseErrors.push(result.parseError);
756
+ for (const warning of result.parseWarnings || []) {
757
+ meta.parseErrors.push({
758
+ file: toRelative(file),
759
+ message: warning,
760
+ });
761
+ }
762
+ findings.push(...result.findings);
763
+ }
764
+
765
+ const summary = {
766
+ findings: findings.length,
767
+ highConfidence: findings.filter((item) => item.confidence === 'high').length,
768
+ mediumConfidence: findings.filter((item) => item.confidence === 'medium').length,
769
+ lowConfidence: findings.filter((item) => item.confidence === 'low').length,
770
+ filesWithFindings: new Set(findings.map((item) => item.file)).size,
771
+ cleanFiles: files.length - new Set(findings.map((item) => item.file)).size,
772
+ };
773
+
774
+ console.log(JSON.stringify({ meta, findings, summary }, null, 2));
775
+ }
776
+
777
+ main();