@wix/agent-skills 1.18.2 → 1.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +42 -40
- package/skills/wix-app/references/BACKEND_API.md +151 -152
- package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +16 -7
- package/skills/wix-app/references/EXTENSION_REGISTRATION.md +12 -8
- package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +53 -131
- package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +1 -2
- package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +83 -73
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +3 -2
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
- package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +1 -1
- package/skills/wix-app/scripts/pkg-root.cjs +73 -0
- package/skills/wix-app/scripts/scan-a11y-code.cjs +310 -40
- package/skills/wix-app/scripts/scan-a11y-eslint.cjs +118 -24
- package/skills/wix-app/scripts/scan-a11y-render.cjs +551 -0
- package/skills/wix-app/scripts/scan-a11y-review.cjs +373 -0
- package/skills/wix-base44-connector/SKILL.md +24 -22
- package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
- package/skills/wix-docs/SKILL.md +47 -20
- package/skills/wix-headless/entry/bootstrap.mjs +106 -50
- package/skills/wix-headless/entry/skill.md +5 -3
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
- package/skills/wix-headless-fast/SKILL.md +25 -7
- package/skills/wix-headless-fast/entry/skill.md +23 -14
- package/skills/wix-headless-fast/install/fast-path.mjs +48 -5
- package/skills/wix-headless-fast/install/pin-agents-md.mjs +57 -0
- package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +3 -3
- package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +204 -35
- package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +146 -36
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +8 -2
- package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +95 -20
- package/skills/wix-headless-fast/references/storefront/seed/SEED.md +21 -2
- package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +104 -18
- package/skills/wix-manage/SKILL.md +14 -8
- package/skills/wix-manage/references/bookings/booking-service-policy-setup.md +59 -33
- package/skills/wix-manage/references/bookings/bookings-staff-setup.md +45 -22
- package/skills/wix-manage/references/bookings/multi-resource-service-creation.md +70 -12
- package/skills/wix-manage/references/calendar/configure-default-business-hours.md +53 -17
- package/skills/wix-manage/references/google-ads/billing-and-payment.md +3 -3
- package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +8 -8
- package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +4 -4
- package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +1 -1
- package/skills/wix-manage/references/google-ads/install-and-create-account.md +9 -9
- package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
- package/skills/wix-manage/references/seo/manage-content-plan.md +181 -135
- package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
- package/skills/wix-manage/references/sites/upload-static-site.md +53 -0
- package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
- package/skills/wix-manage/references/stores/create-product-catalog-v3.md +17 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +12 -9
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +12 -9
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44.md +11 -3
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +5 -1
- package/skills/wix-vibe-headless/references/storefront/seed/SEED.md +5 -4
- package/skills/wix-vibe-headless/references/storefront/seed/seed-store.cjs +33 -5
- package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +0 -80
- package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +0 -140
- package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +0 -112
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +0 -100
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +0 -130
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +0 -48
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +0 -66
- package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +0 -103
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +0 -56
- package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +0 -23
|
@@ -195,6 +195,12 @@ and do not expose a visual prop solely for that purpose.
|
|
|
195
195
|
}
|
|
196
196
|
```
|
|
197
197
|
|
|
198
|
+
The same override risk applies to paintable properties. A layout-only inner
|
|
199
|
+
wrapper must stay visually transparent; if it isn't, promote it to its own
|
|
200
|
+
named part instead of hardcoding an opaque style on an unexposed node, since
|
|
201
|
+
that style would otherwise mask the parent part's own
|
|
202
|
+
`background`/`border`/`box-shadow`.
|
|
203
|
+
|
|
198
204
|
## Interaction and Motion
|
|
199
205
|
|
|
200
206
|
- Set `pointer-events: auto` on the root and each interactive element.
|
|
@@ -254,3 +260,6 @@ contract; the contrast relationship is.
|
|
|
254
260
|
- [ ] Visual containers have deliberate hierarchy rather than bare scaffolding.
|
|
255
261
|
- [ ] Every explicit foreground has a known contrasting background; transparent
|
|
256
262
|
components inherit foregrounds from their host.
|
|
263
|
+
- [ ] No unexposed descendant inside a named part paints its own opaque
|
|
264
|
+
`background`/`border`/`box-shadow`; a wrapper needing its own surface is
|
|
265
|
+
promoted to a named part instead.
|
|
@@ -24,14 +24,17 @@ would plausibly control independently in the editor. It receives:
|
|
|
24
24
|
|
|
25
25
|
- one prefixed global class: `'<component-name>-<part-name>'`
|
|
26
26
|
- one CSS Module class
|
|
27
|
-
- one matching `elementProps` entry, spread onto the element
|
|
27
|
+
- one matching `elementProps` entry, spread onto the element. Declare `a11y` on
|
|
28
|
+
the entry only when the part reads `ariaLabel`, and destructure it out before
|
|
29
|
+
spreading (see `ACCESSIBILITY.md`)
|
|
28
30
|
|
|
29
31
|
Apply this filter to every candidate:
|
|
30
32
|
|
|
31
33
|
- A state or variant (`selected`, `active`, `open`, `disabled`) is not a part.
|
|
32
34
|
Implement it as a prefixed design-state modifier on the affected part.
|
|
33
35
|
- A hidden and shown version of the same element is one part, not two.
|
|
34
|
-
- A grouping or layout-only wrapper is not a part.
|
|
36
|
+
- A grouping or layout-only wrapper is not a part. A wrapper that isn't
|
|
37
|
+
visually transparent is no longer layout-only — promote it to a named part.
|
|
35
38
|
- A static child whose styling and data are fully owned by its parent is not a
|
|
36
39
|
part. For example, an image whose source and appearance both belong to its
|
|
37
40
|
carousel slide can remain module-class-only.
|
|
@@ -86,4 +86,4 @@ export const ProfileCard: React.FC<ProfileCardProps> = (props) => {
|
|
|
86
86
|
- [ ] Direction and accessibility contracts are wired to the correct elements.
|
|
87
87
|
- [ ] Public handlers use supported SDK names; internal handlers stay internal.
|
|
88
88
|
- [ ] CSS changed by the request keeps editable selectors flat and scoped.
|
|
89
|
-
- [ ] The accessibility review
|
|
89
|
+
- [ ] The accessibility review command passes, or its remaining findings are triaged.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Prints the install directory of a package — the folder holding its
|
|
3
|
+
// package.json — resolved from the current working directory.
|
|
4
|
+
//
|
|
5
|
+
// node pkg-root.cjs @wix/patterns
|
|
6
|
+
//
|
|
7
|
+
// Takes the package name as an argument and knows nothing about any particular
|
|
8
|
+
// package, so it stays correct as the packages it is pointed at change.
|
|
9
|
+
//
|
|
10
|
+
// Three steps, because one is not enough in the repos this runs in:
|
|
11
|
+
// 1. Activate Yarn PnP if a .pnp.cjs exists at or above cwd. A bare
|
|
12
|
+
// require.resolve throws in a Yarn Berry project even when the package is
|
|
13
|
+
// installed, and in a monorepo the .pnp.cjs sits at the repo root rather
|
|
14
|
+
// than beside the project being built.
|
|
15
|
+
// 2. require.resolve, which handles npm, pnpm and workspace layouts.
|
|
16
|
+
// 3. Failing that, walk up looking for node_modules/<pkg> directly, which
|
|
17
|
+
// covers a partially-installed tree where resolution fails but the files
|
|
18
|
+
// are on disk.
|
|
19
|
+
const fs = require('fs');
|
|
20
|
+
const path = require('path');
|
|
21
|
+
|
|
22
|
+
const pkg = process.argv[2];
|
|
23
|
+
if (!pkg) {
|
|
24
|
+
console.error('usage: pkg-root.cjs <package-name>');
|
|
25
|
+
process.exit(2);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function activatePnp() {
|
|
29
|
+
let dir = process.cwd();
|
|
30
|
+
for (;;) {
|
|
31
|
+
const pnp = path.join(dir, '.pnp.cjs');
|
|
32
|
+
if (fs.existsSync(pnp)) {
|
|
33
|
+
try {
|
|
34
|
+
require(pnp).setup();
|
|
35
|
+
} catch {
|
|
36
|
+
// An unusable .pnp.cjs is not fatal: the plain resolve below may still
|
|
37
|
+
// succeed against a node_modules tree.
|
|
38
|
+
}
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(dir);
|
|
42
|
+
if (parent === dir) return;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
activatePnp();
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
console.log(
|
|
51
|
+
path.dirname(
|
|
52
|
+
require.resolve(`${pkg}/package.json`, { paths: [process.cwd()] }),
|
|
53
|
+
),
|
|
54
|
+
);
|
|
55
|
+
process.exit(0);
|
|
56
|
+
} catch {
|
|
57
|
+
// fall through to the directory walk
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let dir = process.cwd();
|
|
61
|
+
for (;;) {
|
|
62
|
+
const candidate = path.join(dir, 'node_modules', ...pkg.split('/'));
|
|
63
|
+
if (fs.existsSync(path.join(candidate, 'package.json'))) {
|
|
64
|
+
console.log(candidate);
|
|
65
|
+
process.exit(0);
|
|
66
|
+
}
|
|
67
|
+
const parent = path.dirname(dir);
|
|
68
|
+
if (parent === dir) {
|
|
69
|
+
console.error(`${pkg} not found from ${process.cwd()}`);
|
|
70
|
+
process.exit(1);
|
|
71
|
+
}
|
|
72
|
+
dir = parent;
|
|
73
|
+
}
|
|
@@ -157,6 +157,21 @@ const UNSUPPORTED_ARIA_ELEMENTS = new Set([
|
|
|
157
157
|
]);
|
|
158
158
|
|
|
159
159
|
const FILE_EXTENSIONS = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs'];
|
|
160
|
+
/** `A11y` fields a component may read. ZeroConfig turns each read into an editor control. */
|
|
161
|
+
const A11Y_FIELDS = new Set(['ariaLabel']);
|
|
162
|
+
const A11Y_FIELDS_LABEL = [...A11Y_FIELDS].join(', ');
|
|
163
|
+
const A11Y_CONVERTER = 'convertA11yKeysToHtmlFormat';
|
|
164
|
+
const HARDCODED_LABEL_ATTRIBUTES = new Set(['aria-label', 'aria-description']);
|
|
165
|
+
const SUPPORTED_RULES = [
|
|
166
|
+
'alt-text',
|
|
167
|
+
'anchor-is-valid',
|
|
168
|
+
'aria-props',
|
|
169
|
+
'aria-role',
|
|
170
|
+
'aria-unsupported-elements',
|
|
171
|
+
'a11y-whole-object',
|
|
172
|
+
'a11y-disallowed-field',
|
|
173
|
+
'hardcoded-aria-label',
|
|
174
|
+
];
|
|
160
175
|
const parseCache = new Map();
|
|
161
176
|
const resolutionWarnings = [];
|
|
162
177
|
|
|
@@ -189,12 +204,8 @@ function parseFile(filePath) {
|
|
|
189
204
|
'optionalChaining',
|
|
190
205
|
'nullishCoalescingOperator',
|
|
191
206
|
],
|
|
192
|
-
errorRecovery: true,
|
|
193
207
|
});
|
|
194
|
-
const
|
|
195
|
-
? ast.errors.map((error) => error.message)
|
|
196
|
-
: [];
|
|
197
|
-
const parsed = { ok: true, ast, code, recoverableErrors };
|
|
208
|
+
const parsed = { ok: true, ast, code };
|
|
198
209
|
parseCache.set(filePath, parsed);
|
|
199
210
|
return parsed;
|
|
200
211
|
} catch (error) {
|
|
@@ -557,6 +568,132 @@ function toRelative(filePath) {
|
|
|
557
568
|
return path.relative(ROOT, filePath) || filePath;
|
|
558
569
|
}
|
|
559
570
|
|
|
571
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
572
|
+
// Per-part a11y contract helpers
|
|
573
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
574
|
+
|
|
575
|
+
/** Peel type assertions, parentheses, and guards such as `a11y && x` or `x ?? {}`. */
|
|
576
|
+
function unwrapExpression(node) {
|
|
577
|
+
let current = node;
|
|
578
|
+
for (let guard = 0; current && guard < 20; guard++) {
|
|
579
|
+
if (
|
|
580
|
+
t.isTSAsExpression(current) ||
|
|
581
|
+
t.isTSNonNullExpression(current) ||
|
|
582
|
+
t.isTSTypeAssertion(current) ||
|
|
583
|
+
t.isParenthesizedExpression(current) ||
|
|
584
|
+
(t.isTSSatisfiesExpression && t.isTSSatisfiesExpression(current))
|
|
585
|
+
) {
|
|
586
|
+
current = current.expression;
|
|
587
|
+
} else if (t.isLogicalExpression(current)) {
|
|
588
|
+
current = current.operator === '&&' ? current.right : current.left;
|
|
589
|
+
} else {
|
|
590
|
+
break;
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
return current;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function isA11yIdentifierName(name) {
|
|
597
|
+
return name === 'a11y' || name.endsWith('A11y');
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/** `a11y`, `toggleA11y`, `props.a11y`, `elementProps?.toggle?.a11y`. */
|
|
601
|
+
function isA11yLike(node) {
|
|
602
|
+
const expr = unwrapExpression(node);
|
|
603
|
+
if (t.isIdentifier(expr)) return isA11yIdentifierName(expr.name);
|
|
604
|
+
if ((t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) && !expr.computed) {
|
|
605
|
+
return t.isIdentifier(expr.property, { name: 'a11y' });
|
|
606
|
+
}
|
|
607
|
+
return false;
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/** `props.elementProps?.toggle` → ['props', 'elementProps', 'toggle']; null for other shapes. */
|
|
611
|
+
function memberChain(node) {
|
|
612
|
+
const expr = unwrapExpression(node);
|
|
613
|
+
if (t.isIdentifier(expr)) return [expr.name];
|
|
614
|
+
if ((t.isMemberExpression(expr) || t.isOptionalMemberExpression(expr)) && !expr.computed) {
|
|
615
|
+
const parent = memberChain(expr.object);
|
|
616
|
+
if (!parent || !t.isIdentifier(expr.property)) return null;
|
|
617
|
+
return [...parent, expr.property.name];
|
|
618
|
+
}
|
|
619
|
+
return null;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function isElementPropsPartChain(chain, partsWithA11y) {
|
|
623
|
+
if (!chain || chain.length < 2) return false;
|
|
624
|
+
const index = chain.indexOf('elementProps');
|
|
625
|
+
return index !== -1 && index === chain.length - 2 && partsWithA11y.has(chain[chain.length - 1]);
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function typeLiteralHasMember(typeNode, memberName) {
|
|
629
|
+
return (
|
|
630
|
+
t.isTSTypeLiteral(typeNode) &&
|
|
631
|
+
typeNode.members.some(
|
|
632
|
+
(member) =>
|
|
633
|
+
t.isTSPropertySignature(member) &&
|
|
634
|
+
((t.isIdentifier(member.key) && member.key.name === memberName) ||
|
|
635
|
+
(t.isStringLiteral(member.key) && member.key.value === memberName)),
|
|
636
|
+
)
|
|
637
|
+
);
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Names of `elementProps` parts whose declared type carries an `a11y` field,
|
|
642
|
+
* keyed by folder so one component's contract never applies to another.
|
|
643
|
+
* Reads every `*.props.ts` next to the scanned files plus the files themselves.
|
|
644
|
+
*/
|
|
645
|
+
function collectPropsTypes(files) {
|
|
646
|
+
const partsByDir = new Map();
|
|
647
|
+
const candidates = new Set(files);
|
|
648
|
+
for (const file of files) {
|
|
649
|
+
const dir = path.dirname(file);
|
|
650
|
+
if (!partsByDir.has(dir)) partsByDir.set(dir, new Set());
|
|
651
|
+
if (!fs.existsSync(dir)) continue;
|
|
652
|
+
for (const name of fs.readdirSync(dir)) {
|
|
653
|
+
if (name.endsWith('.props.ts')) candidates.add(path.join(dir, name));
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
for (const file of candidates) {
|
|
658
|
+
const parsed = parseFile(file);
|
|
659
|
+
if (!parsed.ok) continue;
|
|
660
|
+
const partsWithA11y = partsByDir.get(path.dirname(file));
|
|
661
|
+
traverse(parsed.ast, {
|
|
662
|
+
TSPropertySignature(path) {
|
|
663
|
+
const { key, typeAnnotation } = path.node;
|
|
664
|
+
if (!t.isIdentifier(key, { name: 'elementProps' }) || !typeAnnotation) return;
|
|
665
|
+
const literal = typeAnnotation.typeAnnotation;
|
|
666
|
+
if (!t.isTSTypeLiteral(literal)) return;
|
|
667
|
+
for (const member of literal.members) {
|
|
668
|
+
if (!t.isTSPropertySignature(member) || !member.typeAnnotation) continue;
|
|
669
|
+
const partName = t.isIdentifier(member.key)
|
|
670
|
+
? member.key.name
|
|
671
|
+
: t.isStringLiteral(member.key)
|
|
672
|
+
? member.key.value
|
|
673
|
+
: null;
|
|
674
|
+
if (partName && typeLiteralHasMember(member.typeAnnotation.typeAnnotation, 'a11y')) {
|
|
675
|
+
partsWithA11y.add(partName);
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
},
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
return partsByDir;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function contractFinding(filePath, node, rule, confidence, message) {
|
|
686
|
+
return findingFromNode(filePath, node, {
|
|
687
|
+
rule,
|
|
688
|
+
confidence,
|
|
689
|
+
message,
|
|
690
|
+
componentName: null,
|
|
691
|
+
semanticType: 'a11y-contract',
|
|
692
|
+
evidence: 'Per-part accessibility contract (ACCESSIBILITY.md).',
|
|
693
|
+
sourceKind: 'contract',
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
560
697
|
function findingFromNode(filePath, node, data) {
|
|
561
698
|
return {
|
|
562
699
|
file: toRelative(filePath),
|
|
@@ -572,7 +709,7 @@ function findingFromNode(filePath, node, data) {
|
|
|
572
709
|
};
|
|
573
710
|
}
|
|
574
711
|
|
|
575
|
-
function scanFile(filePath) {
|
|
712
|
+
function scanFile(filePath, options = {}) {
|
|
576
713
|
const parsed = parseFile(filePath);
|
|
577
714
|
if (!parsed.ok) {
|
|
578
715
|
return {
|
|
@@ -586,9 +723,124 @@ function scanFile(filePath) {
|
|
|
586
723
|
|
|
587
724
|
const imports = getImportMap(parsed.ast);
|
|
588
725
|
const findings = [];
|
|
589
|
-
const
|
|
726
|
+
const partsWithA11y = options.partsWithA11y || new Set();
|
|
727
|
+
|
|
728
|
+
const reportDisallowedField = (node, fieldName) => {
|
|
729
|
+
findings.push(
|
|
730
|
+
contractFinding(
|
|
731
|
+
filePath,
|
|
732
|
+
node,
|
|
733
|
+
'a11y-disallowed-field',
|
|
734
|
+
'high',
|
|
735
|
+
`a11y.${fieldName} is read; only ${A11Y_FIELDS_LABEL} may be read. Keep roles, state, and structure in component code.`,
|
|
736
|
+
),
|
|
737
|
+
);
|
|
738
|
+
};
|
|
590
739
|
|
|
591
740
|
traverse(parsed.ast, {
|
|
741
|
+
JSXSpreadAttribute(path) {
|
|
742
|
+
const argument = unwrapExpression(path.node.argument);
|
|
743
|
+
|
|
744
|
+
if (isA11yLike(argument)) {
|
|
745
|
+
findings.push(
|
|
746
|
+
contractFinding(
|
|
747
|
+
filePath,
|
|
748
|
+
path.node,
|
|
749
|
+
'a11y-whole-object',
|
|
750
|
+
'high',
|
|
751
|
+
'The whole a11y object is spread onto an element; every field becomes an editor control.',
|
|
752
|
+
),
|
|
753
|
+
);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
|
|
757
|
+
// A spread identifier may alias `a11y` or an `elementProps` part; follow its declaration.
|
|
758
|
+
let chain = memberChain(argument);
|
|
759
|
+
if (t.isIdentifier(argument)) {
|
|
760
|
+
const binding = path.scope.getBinding(argument.name);
|
|
761
|
+
const declarator = binding && binding.path && binding.path.node;
|
|
762
|
+
if (declarator && t.isVariableDeclarator(declarator) && t.isIdentifier(declarator.id)) {
|
|
763
|
+
if (declarator.init && isA11yLike(declarator.init)) {
|
|
764
|
+
findings.push(
|
|
765
|
+
contractFinding(
|
|
766
|
+
filePath,
|
|
767
|
+
path.node,
|
|
768
|
+
'a11y-whole-object',
|
|
769
|
+
'high',
|
|
770
|
+
`${argument.name} aliases the a11y object and is spread onto an element; read a11y.ariaLabel instead.`,
|
|
771
|
+
),
|
|
772
|
+
);
|
|
773
|
+
return;
|
|
774
|
+
}
|
|
775
|
+
chain = memberChain(declarator.init);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
if (isElementPropsPartChain(chain, partsWithA11y)) {
|
|
779
|
+
findings.push(
|
|
780
|
+
contractFinding(
|
|
781
|
+
filePath,
|
|
782
|
+
path.node,
|
|
783
|
+
'a11y-whole-object',
|
|
784
|
+
'medium',
|
|
785
|
+
`elementProps.${chain[chain.length - 1]} is spread although its type declares a11y; destructure a11y out first.`,
|
|
786
|
+
),
|
|
787
|
+
);
|
|
788
|
+
}
|
|
789
|
+
},
|
|
790
|
+
|
|
791
|
+
CallExpression(path) {
|
|
792
|
+
const callee = path.node.callee;
|
|
793
|
+
const isConverter =
|
|
794
|
+
t.isIdentifier(callee, { name: A11Y_CONVERTER }) ||
|
|
795
|
+
((t.isMemberExpression(callee) || t.isOptionalMemberExpression(callee)) &&
|
|
796
|
+
t.isIdentifier(callee.property, { name: A11Y_CONVERTER }));
|
|
797
|
+
if (isConverter) {
|
|
798
|
+
findings.push(
|
|
799
|
+
contractFinding(
|
|
800
|
+
filePath,
|
|
801
|
+
path.node,
|
|
802
|
+
'a11y-whole-object',
|
|
803
|
+
'high',
|
|
804
|
+
`${A11Y_CONVERTER} writes every a11y field to the DOM; read only the field the part needs.`,
|
|
805
|
+
),
|
|
806
|
+
);
|
|
807
|
+
}
|
|
808
|
+
},
|
|
809
|
+
|
|
810
|
+
'MemberExpression|OptionalMemberExpression'(path) {
|
|
811
|
+
const { node } = path;
|
|
812
|
+
if (node.computed || !t.isIdentifier(node.property)) return;
|
|
813
|
+
if (!isA11yLike(node.object)) return;
|
|
814
|
+
if (A11Y_FIELDS.has(node.property.name)) return;
|
|
815
|
+
reportDisallowedField(node.property, node.property.name);
|
|
816
|
+
},
|
|
817
|
+
|
|
818
|
+
VariableDeclarator(path) {
|
|
819
|
+
const { id, init } = path.node;
|
|
820
|
+
if (!t.isObjectPattern(id) || !init || !isA11yLike(init)) return;
|
|
821
|
+
for (const property of id.properties) {
|
|
822
|
+
if (t.isRestElement(property)) {
|
|
823
|
+
findings.push(
|
|
824
|
+
contractFinding(
|
|
825
|
+
filePath,
|
|
826
|
+
property,
|
|
827
|
+
'a11y-whole-object',
|
|
828
|
+
'high',
|
|
829
|
+
'Rest-destructuring a11y keeps every field; pick the single field the part needs.',
|
|
830
|
+
),
|
|
831
|
+
);
|
|
832
|
+
continue;
|
|
833
|
+
}
|
|
834
|
+
if (!t.isObjectProperty(property) || property.computed) continue;
|
|
835
|
+
const fieldName = t.isIdentifier(property.key)
|
|
836
|
+
? property.key.name
|
|
837
|
+
: t.isStringLiteral(property.key)
|
|
838
|
+
? property.key.value
|
|
839
|
+
: null;
|
|
840
|
+
if (fieldName && !A11Y_FIELDS.has(fieldName)) reportDisallowedField(property, fieldName);
|
|
841
|
+
}
|
|
842
|
+
},
|
|
843
|
+
|
|
592
844
|
JSXOpeningElement(path) {
|
|
593
845
|
const node = path.node;
|
|
594
846
|
const name = getJsxName(node.name);
|
|
@@ -600,6 +852,23 @@ function scanFile(filePath) {
|
|
|
600
852
|
);
|
|
601
853
|
const roleAttr = getAttribute(node, 'role');
|
|
602
854
|
|
|
855
|
+
for (const attr of ariaAttrs) {
|
|
856
|
+
const attrName = getJsxName(attr.name);
|
|
857
|
+
if (!HARDCODED_LABEL_ATTRIBUTES.has(attrName)) continue;
|
|
858
|
+
const literal = getLiteralAttributeValue(attr);
|
|
859
|
+
if (typeof literal === 'string' && literal.trim() !== '') {
|
|
860
|
+
findings.push(
|
|
861
|
+
contractFinding(
|
|
862
|
+
filePath,
|
|
863
|
+
attr,
|
|
864
|
+
'hardcoded-aria-label',
|
|
865
|
+
'high',
|
|
866
|
+
`${attrName}="${literal}" is a hardcoded string; use visible text, a11y.ariaLabel, or a constants.ts label.`,
|
|
867
|
+
),
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
|
|
603
872
|
if (semantic.semanticType === 'img' && semantic.confidence !== 'low') {
|
|
604
873
|
const altAttr = getAttribute(node, 'alt');
|
|
605
874
|
if (!altAttr) {
|
|
@@ -714,35 +983,19 @@ function scanFile(filePath) {
|
|
|
714
983
|
},
|
|
715
984
|
});
|
|
716
985
|
|
|
717
|
-
return { parseError: null, findings
|
|
986
|
+
return { parseError: null, findings };
|
|
718
987
|
}
|
|
719
988
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
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
|
-
}
|
|
989
|
+
/** Scan the given files. Any syntax error lands in `meta.parseErrors`; the review treats it as fatal. */
|
|
990
|
+
function scan(files) {
|
|
991
|
+
const absoluteFiles = files.map((file) => path.resolve(file));
|
|
992
|
+
const partsByDir = collectPropsTypes(absoluteFiles);
|
|
735
993
|
|
|
736
994
|
const meta = {
|
|
737
995
|
filesScanned: files.length,
|
|
738
996
|
parser: '@babel/parser',
|
|
739
|
-
supportedRules:
|
|
740
|
-
|
|
741
|
-
'anchor-is-valid',
|
|
742
|
-
'aria-props',
|
|
743
|
-
'aria-role',
|
|
744
|
-
'aria-unsupported-elements',
|
|
745
|
-
],
|
|
997
|
+
supportedRules: SUPPORTED_RULES,
|
|
998
|
+
a11yFields: [...A11Y_FIELDS],
|
|
746
999
|
confidenceModel: ['high', 'medium', 'low', 'unknown'],
|
|
747
1000
|
parseErrors: [],
|
|
748
1001
|
resolutionWarnings,
|
|
@@ -750,15 +1003,9 @@ function main() {
|
|
|
750
1003
|
|
|
751
1004
|
const findings = [];
|
|
752
1005
|
|
|
753
|
-
for (const file of
|
|
754
|
-
const result = scanFile(file);
|
|
1006
|
+
for (const file of absoluteFiles) {
|
|
1007
|
+
const result = scanFile(file, { partsWithA11y: partsByDir.get(path.dirname(file)) });
|
|
755
1008
|
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
1009
|
findings.push(...result.findings);
|
|
763
1010
|
}
|
|
764
1011
|
|
|
@@ -771,7 +1018,30 @@ function main() {
|
|
|
771
1018
|
cleanFiles: files.length - new Set(findings.map((item) => item.file)).size,
|
|
772
1019
|
};
|
|
773
1020
|
|
|
774
|
-
|
|
1021
|
+
return { meta, findings, summary };
|
|
775
1022
|
}
|
|
776
1023
|
|
|
777
|
-
main()
|
|
1024
|
+
function main() {
|
|
1025
|
+
const files = process.argv.slice(2);
|
|
1026
|
+
if (files.length === 0) {
|
|
1027
|
+
console.log(
|
|
1028
|
+
JSON.stringify(
|
|
1029
|
+
{
|
|
1030
|
+
error: 'No files specified.',
|
|
1031
|
+
usage: 'node <SKILL_ROOT>/scripts/scan-a11y-code.cjs <file1> [file2] ...',
|
|
1032
|
+
},
|
|
1033
|
+
null,
|
|
1034
|
+
2,
|
|
1035
|
+
),
|
|
1036
|
+
);
|
|
1037
|
+
process.exit(1);
|
|
1038
|
+
}
|
|
1039
|
+
|
|
1040
|
+
console.log(JSON.stringify(scan(files), null, 2));
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
module.exports = { scan };
|
|
1044
|
+
|
|
1045
|
+
if (require.main === module) {
|
|
1046
|
+
main();
|
|
1047
|
+
}
|