@wix/agent-skills 1.19.0 → 1.19.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.
- package/package.json +1 -1
- package/skills/wix-app/SKILL.md +33 -31
- package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
- package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +17 -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 +88 -73
- package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +46 -22
- package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +20 -6
- package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
- package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +71 -31
- package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
- package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +4 -5
- 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 +67 -43
- package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
- package/skills/wix-docs/SKILL.md +47 -20
- package/skills/wix-headless/SKILL.md +1 -0
- package/skills/wix-headless/entry/bootstrap.mjs +106 -50
- package/skills/wix-headless/entry/skill.md +5 -3
- package/skills/wix-headless/references/CAPABILITIES.md +7 -2
- package/skills/wix-headless/references/SDK_HANDOFF.md +2 -1
- package/skills/wix-headless/references/inline-recipes/experience-store.md +48 -0
- package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +350 -114
- package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +1 -1
- package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
- package/skills/wix-headless/references/managed/CREATE.md +5 -0
- 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 +16 -10
- package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +4 -1
- package/skills/wix-manage/references/bookings/create-appointment-service.md +3 -1
- package/skills/wix-manage/references/bookings/create-class-service.md +3 -1
- package/skills/wix-manage/references/bookings/create-course-service.md +4 -2
- package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +68 -3
- package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-seasonal-promotion.md +1 -1
- 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/manage-campaign-success-guide.md +108 -14
- package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
- package/skills/wix-manage/references/seo/manage-content-plan.md +182 -136
- package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
- package/skills/wix-manage/references/sites/upload-static-site.md +283 -0
- package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +3 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +3 -1
- package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
- package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +69 -27
- package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +32 -14
- package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +14 -3
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +41 -35
- package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +77 -30
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +31 -2
- package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +31 -6
- 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
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
// oxlint-disable no-console preserve-caught-error
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Accessibility review for Editor React Components: one command, one JSON report.
|
|
8
|
+
*
|
|
9
|
+
* node <SKILL_ROOT>/scripts/scan-a11y-review.cjs <component-dir | files...>
|
|
10
|
+
*
|
|
11
|
+
* Runs the jsx-a11y ESLint scanner and the Babel semantic/contract scanner in
|
|
12
|
+
* process, spawns the render audit (SSR + jsdom + axe-core) per component
|
|
13
|
+
* folder, and groups the findings by rule.
|
|
14
|
+
*
|
|
15
|
+
* Exit codes: 0 every scanner ran and found nothing, 1 findings, 2 a scanner
|
|
16
|
+
* could not run or could not load the component (never treat 2 as clean).
|
|
17
|
+
* Test hook: `A11Y_SKIP_RENDER=1` skips the render audit; the review is then
|
|
18
|
+
* inconclusive (exit 2) even with no findings.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const fs = require('fs');
|
|
22
|
+
const path = require('path');
|
|
23
|
+
const { spawnSync } = require('child_process');
|
|
24
|
+
|
|
25
|
+
const ROOT = process.cwd();
|
|
26
|
+
const RENDER_SCRIPT = path.join(__dirname, 'scan-a11y-render.cjs');
|
|
27
|
+
const RENDER_TIMEOUT_MS = Number(process.env.A11Y_RENDER_TIMEOUT_MS) || 20_000;
|
|
28
|
+
const MAX_LOCATIONS_PER_GROUP = 5;
|
|
29
|
+
const SEVERITY_RANK = { high: 0, medium: 1, low: 2 };
|
|
30
|
+
const CONFIDENCE_RANK = { high: 0, medium: 1, low: 2, unknown: 3 };
|
|
31
|
+
|
|
32
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
+
// Arguments and discovery
|
|
34
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
35
|
+
|
|
36
|
+
function parseArgs(argv) {
|
|
37
|
+
const options = { targets: [], render: process.env.A11Y_SKIP_RENDER !== '1' };
|
|
38
|
+
for (const arg of argv) {
|
|
39
|
+
if (arg.startsWith('--')) throw new Error(`Unknown option ${arg}`);
|
|
40
|
+
options.targets.push(arg);
|
|
41
|
+
}
|
|
42
|
+
return options;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const isSource = (name) =>
|
|
46
|
+
/\.(tsx|jsx)$/.test(name) && !/\.(stories|test|spec)\.[jt]sx$/.test(name);
|
|
47
|
+
|
|
48
|
+
function listSources(dir, acc = []) {
|
|
49
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
50
|
+
const full = path.join(dir, entry.name);
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
if (entry.name !== 'node_modules') listSources(full, acc);
|
|
53
|
+
} else if (entry.isFile() && isSource(entry.name)) acc.push(full);
|
|
54
|
+
}
|
|
55
|
+
return acc;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function isComponentDir(dir) {
|
|
59
|
+
if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) return false;
|
|
60
|
+
const names = fs.readdirSync(dir);
|
|
61
|
+
return names.includes('component.tsx') || names.some((name) => name.endsWith('.props.ts'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function findComponentDir(file) {
|
|
65
|
+
let dir = path.dirname(file);
|
|
66
|
+
for (let depth = 0; depth < 6 && !isComponentDir(dir); depth++) {
|
|
67
|
+
const parent = path.dirname(dir);
|
|
68
|
+
if (parent === dir) return path.dirname(file);
|
|
69
|
+
dir = parent;
|
|
70
|
+
}
|
|
71
|
+
return isComponentDir(dir) ? dir : path.dirname(file);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Expand targets (component folders, a components root, or files) into files and component folders. */
|
|
75
|
+
function discoverFiles(targets) {
|
|
76
|
+
const files = new Set();
|
|
77
|
+
const componentDirs = new Set();
|
|
78
|
+
for (const target of targets) {
|
|
79
|
+
const resolved = path.resolve(target);
|
|
80
|
+
if (!fs.existsSync(resolved)) throw new Error(`Path not found: ${target}`);
|
|
81
|
+
if (fs.statSync(resolved).isDirectory()) {
|
|
82
|
+
if (isComponentDir(resolved)) componentDirs.add(resolved);
|
|
83
|
+
else {
|
|
84
|
+
for (const entry of fs.readdirSync(resolved, { withFileTypes: true })) {
|
|
85
|
+
const child = path.join(resolved, entry.name);
|
|
86
|
+
if (entry.isDirectory() && isComponentDir(child)) componentDirs.add(child);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
for (const file of listSources(resolved)) files.add(file);
|
|
90
|
+
} else {
|
|
91
|
+
files.add(resolved);
|
|
92
|
+
componentDirs.add(findComponentDir(resolved));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return { files: [...files].toSorted(), componentDirs: [...componentDirs].toSorted() };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
+
// Scanners
|
|
100
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
async function runScanner(name, load) {
|
|
103
|
+
const started = Date.now();
|
|
104
|
+
try {
|
|
105
|
+
const value = await load();
|
|
106
|
+
const parseErrors = (value.meta && value.meta.parseErrors) || [];
|
|
107
|
+
if (parseErrors.length) {
|
|
108
|
+
throw new Error(parseErrors.map((e) => `${e.file}: ${e.message}`).join('; '));
|
|
109
|
+
}
|
|
110
|
+
return { ok: true, ms: Date.now() - started, value };
|
|
111
|
+
} catch (error) {
|
|
112
|
+
const message = error && error.message ? error.message : String(error);
|
|
113
|
+
return { ok: false, ms: Date.now() - started, error: `${name}: ${message}` };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function eslintFindings(report) {
|
|
118
|
+
return report.findings.map((f) => ({
|
|
119
|
+
rule: f.rule,
|
|
120
|
+
source: 'eslint',
|
|
121
|
+
severity: 'high',
|
|
122
|
+
confidence: 'high',
|
|
123
|
+
message: f.message,
|
|
124
|
+
at: `${f.file}:${f.line}:${f.column}`,
|
|
125
|
+
}));
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function semanticFindings(report) {
|
|
129
|
+
return report.findings.map((f) => {
|
|
130
|
+
const contract = f.sourceKind === 'contract';
|
|
131
|
+
const severity =
|
|
132
|
+
contract || f.confidence === 'high' ? 'high' : f.confidence === 'medium' ? 'medium' : 'low';
|
|
133
|
+
return {
|
|
134
|
+
rule: f.rule,
|
|
135
|
+
source: contract ? 'contract' : 'semantic',
|
|
136
|
+
severity,
|
|
137
|
+
confidence: f.confidence,
|
|
138
|
+
message: f.message,
|
|
139
|
+
detail: contract ? null : f.evidence,
|
|
140
|
+
at: `${f.file}:${f.line}:${f.column}`,
|
|
141
|
+
};
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function runRenderChild(componentDir) {
|
|
146
|
+
const relativeDir = path.relative(ROOT, componentDir) || '.';
|
|
147
|
+
const started = Date.now();
|
|
148
|
+
const result = spawnSync(process.execPath, [RENDER_SCRIPT, componentDir], {
|
|
149
|
+
cwd: ROOT,
|
|
150
|
+
encoding: 'utf8',
|
|
151
|
+
timeout: RENDER_TIMEOUT_MS,
|
|
152
|
+
killSignal: 'SIGKILL',
|
|
153
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
154
|
+
env: { ...process.env, NODE_ENV: process.env.NODE_ENV || 'development' },
|
|
155
|
+
});
|
|
156
|
+
const base = { componentDir: relativeDir, ms: Date.now() - started };
|
|
157
|
+
|
|
158
|
+
if (result.error && result.error.code === 'ETIMEDOUT') {
|
|
159
|
+
return {
|
|
160
|
+
...base,
|
|
161
|
+
ok: false,
|
|
162
|
+
reason: 'timeout',
|
|
163
|
+
findings: [
|
|
164
|
+
{
|
|
165
|
+
rule: 'render-timeout',
|
|
166
|
+
source: 'render',
|
|
167
|
+
severity: 'high',
|
|
168
|
+
confidence: 'high',
|
|
169
|
+
message: `Rendering ${relativeDir} did not finish within ${RENDER_TIMEOUT_MS} ms. Rerun once; if it repeats, the first render never settles.`,
|
|
170
|
+
},
|
|
171
|
+
],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
let report = null;
|
|
176
|
+
try {
|
|
177
|
+
const lastLine = String(result.stdout || '')
|
|
178
|
+
.trim()
|
|
179
|
+
.split('\n')
|
|
180
|
+
.filter(Boolean)
|
|
181
|
+
.pop();
|
|
182
|
+
report = lastLine ? JSON.parse(lastLine) : null;
|
|
183
|
+
} catch {
|
|
184
|
+
report = null;
|
|
185
|
+
}
|
|
186
|
+
if (!report) {
|
|
187
|
+
const error = truncate(result.stderr || `exit ${result.status}`, 400);
|
|
188
|
+
return { ...base, ok: false, reason: 'crash', error, findings: [] };
|
|
189
|
+
}
|
|
190
|
+
if (!report.ok) {
|
|
191
|
+
return {
|
|
192
|
+
...base,
|
|
193
|
+
ok: false,
|
|
194
|
+
reason: report.reason || 'error',
|
|
195
|
+
error: report.error || null,
|
|
196
|
+
findings: [],
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
return {
|
|
200
|
+
...base,
|
|
201
|
+
ok: true,
|
|
202
|
+
entry: report.entry,
|
|
203
|
+
preview: report.preview,
|
|
204
|
+
ssr: report.ssr,
|
|
205
|
+
axeRules: report.axe ? report.axe.rulesRun : null,
|
|
206
|
+
notChecked: report.notChecked || [],
|
|
207
|
+
findings: report.findings.map((f) => ({
|
|
208
|
+
...f,
|
|
209
|
+
source: 'render',
|
|
210
|
+
at: f.target ? `${relativeDir} ${f.target}` : relativeDir,
|
|
211
|
+
})),
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
// Report
|
|
217
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
function truncate(text, length) {
|
|
220
|
+
const compact = String(text || '').replace(/\s+/g, ' ');
|
|
221
|
+
return compact.length > length ? `${compact.slice(0, length - 1)}…` : compact;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/** One entry per (source, rule, message); repeated nodes become a count plus a few locations. */
|
|
225
|
+
function groupFindings(findings, { capPerGroup = MAX_LOCATIONS_PER_GROUP } = {}) {
|
|
226
|
+
const groups = new Map();
|
|
227
|
+
for (const f of findings) {
|
|
228
|
+
const severity = f.severity in SEVERITY_RANK ? f.severity : 'medium';
|
|
229
|
+
const confidence = f.confidence in CONFIDENCE_RANK ? f.confidence : 'medium';
|
|
230
|
+
const key = `${f.source}|${f.rule}|${f.message}`;
|
|
231
|
+
if (!groups.has(key)) {
|
|
232
|
+
groups.set(key, {
|
|
233
|
+
rule: f.rule,
|
|
234
|
+
src: f.source,
|
|
235
|
+
sev: severity,
|
|
236
|
+
conf: confidence,
|
|
237
|
+
n: 0,
|
|
238
|
+
at: [],
|
|
239
|
+
html: f.html || null,
|
|
240
|
+
msg: f.message,
|
|
241
|
+
detail: f.detail || null,
|
|
242
|
+
helpUrl: f.helpUrl || null,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
const group = groups.get(key);
|
|
246
|
+
group.n++;
|
|
247
|
+
if (f.at && group.at.length < capPerGroup && !group.at.includes(f.at)) group.at.push(f.at);
|
|
248
|
+
if (SEVERITY_RANK[severity] < SEVERITY_RANK[group.sev]) group.sev = severity;
|
|
249
|
+
if (CONFIDENCE_RANK[confidence] < CONFIDENCE_RANK[group.conf]) group.conf = confidence;
|
|
250
|
+
}
|
|
251
|
+
return [...groups.values()].toSorted(
|
|
252
|
+
(a, b) =>
|
|
253
|
+
SEVERITY_RANK[a.sev] - SEVERITY_RANK[b.sev] ||
|
|
254
|
+
CONFIDENCE_RANK[a.conf] - CONFIDENCE_RANK[b.conf] ||
|
|
255
|
+
b.n - a.n ||
|
|
256
|
+
a.rule.localeCompare(b.rule),
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function renderLabel(result) {
|
|
261
|
+
if (result.ok) {
|
|
262
|
+
if (!result.ssr || !result.ssr.ok) return `render: ssr FAILED (${result.entry || '?'})`;
|
|
263
|
+
return `render ok (${result.entry || '?'}, axe ${result.axeRules ?? '-'})`;
|
|
264
|
+
}
|
|
265
|
+
return `render FAILED (${result.reason})`;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function buildSummary(groups, scanners, { renderEnabled = true } = {}) {
|
|
269
|
+
const counts = { high: 0, medium: 0, low: 0, groups: groups.length, findings: 0 };
|
|
270
|
+
for (const group of groups) {
|
|
271
|
+
counts[group.sev] += group.n;
|
|
272
|
+
counts.findings += group.n;
|
|
273
|
+
}
|
|
274
|
+
// A timeout is itself a finding; any other render failure (missing tooling,
|
|
275
|
+
// a module the audit cannot load) or a skipped render means the audit did
|
|
276
|
+
// not run, never clean.
|
|
277
|
+
const staticFailed = !scanners.eslint.ok || !scanners.semantic.ok;
|
|
278
|
+
const renderBroken =
|
|
279
|
+
!renderEnabled || scanners.render.some((r) => !r.ok && r.reason !== 'timeout');
|
|
280
|
+
const status =
|
|
281
|
+
staticFailed || renderBroken ? 'error' : counts.findings > 0 ? 'findings' : 'clean';
|
|
282
|
+
const severities = ['high', 'medium', 'low']
|
|
283
|
+
.filter((level) => counts[level] > 0)
|
|
284
|
+
.map((level) => `${counts[level]} ${level}`);
|
|
285
|
+
const line = [
|
|
286
|
+
`A11Y ${counts.groups} groups/${counts.findings} findings${severities.length ? ` (${severities.join(', ')})` : ''}`,
|
|
287
|
+
`eslint ${scanners.eslint.ok ? 'ok' : 'FAILED'}`,
|
|
288
|
+
`semantic ${scanners.semantic.ok ? 'ok' : 'FAILED'}`,
|
|
289
|
+
renderEnabled
|
|
290
|
+
? scanners.render.map(renderLabel).join(' · ') || 'render: no component folder'
|
|
291
|
+
: 'render SKIPPED (inconclusive)',
|
|
292
|
+
].join(' · ');
|
|
293
|
+
return { status, line, counts };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const computeExitCode = (summary) =>
|
|
297
|
+
summary.status === 'error' ? 2 : summary.counts.findings > 0 ? 1 : 0;
|
|
298
|
+
|
|
299
|
+
async function review(options) {
|
|
300
|
+
const { files, componentDirs } = discoverFiles(options.targets);
|
|
301
|
+
// A wrong path (a typo, a parent folder) must never read as clean.
|
|
302
|
+
if (componentDirs.length === 0) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
`No component folder under ${options.targets.join(', ')}: expected component.tsx or *.props.ts.`,
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// ESLint rejects an empty file list; a folder with no JSX still gets its render audit.
|
|
309
|
+
const eslint = await runScanner('eslint', () =>
|
|
310
|
+
files.length ? require('./scan-a11y-eslint.cjs').scan(files) : { findings: [] },
|
|
311
|
+
);
|
|
312
|
+
const semantic = await runScanner('semantic', () =>
|
|
313
|
+
files.length ? require('./scan-a11y-code.cjs').scan(files) : { findings: [] },
|
|
314
|
+
);
|
|
315
|
+
const render = options.render ? componentDirs.map(runRenderChild) : [];
|
|
316
|
+
|
|
317
|
+
const findings = [
|
|
318
|
+
...(eslint.ok ? eslintFindings(eslint.value) : []),
|
|
319
|
+
...(semantic.ok ? semanticFindings(semantic.value) : []),
|
|
320
|
+
...render.flatMap((r) => r.findings),
|
|
321
|
+
];
|
|
322
|
+
const groups = groupFindings(findings);
|
|
323
|
+
|
|
324
|
+
const notChecked = new Set(options.render ? [] : ['render audit (skipped)']);
|
|
325
|
+
for (const r of render) {
|
|
326
|
+
for (const item of r.notChecked || []) notChecked.add(item);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
const scanners = {
|
|
330
|
+
eslint: { ok: eslint.ok, ms: eslint.ms, error: eslint.error || null },
|
|
331
|
+
semantic: { ok: semantic.ok, ms: semantic.ms, error: semantic.error || null },
|
|
332
|
+
render: render.map(({ findings: _findings, notChecked: _notChecked, ...rest }) => rest),
|
|
333
|
+
};
|
|
334
|
+
const summary = buildSummary(groups, scanners, { renderEnabled: options.render });
|
|
335
|
+
return {
|
|
336
|
+
summary,
|
|
337
|
+
findings: groups,
|
|
338
|
+
scanners,
|
|
339
|
+
files: files.map((file) => path.relative(ROOT, file)),
|
|
340
|
+
notChecked: [...notChecked],
|
|
341
|
+
exitCode: computeExitCode(summary),
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function main() {
|
|
346
|
+
let options;
|
|
347
|
+
try {
|
|
348
|
+
options = parseArgs(process.argv.slice(2));
|
|
349
|
+
if (options.targets.length === 0) throw new Error('No component directory or files specified.');
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error(
|
|
352
|
+
`${error.message}\nUsage: node <SKILL_ROOT>/scripts/scan-a11y-review.cjs <component-dir | files...>`,
|
|
353
|
+
);
|
|
354
|
+
process.exit(2);
|
|
355
|
+
}
|
|
356
|
+
try {
|
|
357
|
+
const report = await review(options);
|
|
358
|
+
console.log(JSON.stringify(report));
|
|
359
|
+
process.exit(report.exitCode);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
console.log(
|
|
362
|
+
JSON.stringify({
|
|
363
|
+
summary: { status: 'error', line: error.message, counts: { findings: 0 } },
|
|
364
|
+
findings: [],
|
|
365
|
+
}),
|
|
366
|
+
);
|
|
367
|
+
process.exit(2);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
module.exports = { review, discoverFiles, groupFindings, buildSummary, computeExitCode };
|
|
372
|
+
|
|
373
|
+
if (require.main === module) main();
|
|
@@ -23,8 +23,8 @@ they also surface from a search that began at the methods.
|
|
|
23
23
|
|
|
24
24
|
In this skill:
|
|
25
25
|
|
|
26
|
-
- **What are you building?** —
|
|
27
|
-
|
|
26
|
+
- **What are you building?** — for visitors, for the owner, or management done for them —
|
|
27
|
+
decided per request from the user's intent, each on its own flow
|
|
28
28
|
- **The helpers** — the `wx.*` loader every exec opens with
|
|
29
29
|
- **Gather context** — `wx.context`, the report of what the site actually has
|
|
30
30
|
- **Learn Wix** — find the APIs, learn their contracts
|
|
@@ -40,25 +40,41 @@ In this skill:
|
|
|
40
40
|
|
|
41
41
|
## What are you building?
|
|
42
42
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
43
|
+
Wix has two identities, visitor and admin, and each has its own flow. Decide which one every
|
|
44
|
+
request is for before acting on it — from what the user wants, not from the technology:
|
|
45
|
+
|
|
46
|
+
- **Visitor** — the people using the site: browsing, booking, buying, signing in as a member.
|
|
47
|
+
Visitor token, called from the browser through one shared visitor client.
|
|
48
|
+
- **Admin, built** — a management surface of the owner's own: a custom view, a bulk operation,
|
|
49
|
+
an internal tool. Admin connector token, server-side in backend functions the frontend calls.
|
|
50
|
+
- **Admin, done for them** — the user wants the business managed now, not an app: create the
|
|
51
|
+
products, change a setting, import the contacts. Admin token, ad hoc in exec_tool; nothing
|
|
52
|
+
ships in the app.
|
|
53
|
+
|
|
54
|
+
The owner already has a full Wix dashboard for running the business — orders, products,
|
|
55
|
+
bookings, settings, and more — so an admin build is a choice the user makes, not a gap to fill by
|
|
56
|
+
default. When a request has both visitor and admin parts, each keeps its own flow.
|
|
57
|
+
|
|
58
|
+
When the request doesn't say which, ask the user — in their terms (who the thing is for), not in
|
|
59
|
+
technical ones (identities, tokens, flows). When you decide without asking, tell them what you
|
|
60
|
+
chose the same way.
|
|
61
|
+
|
|
62
|
+
The rules that follow from the split:
|
|
63
|
+
|
|
64
|
+
- **Checkout runs on a visitor token, never the admin token** — including a checkout this app
|
|
65
|
+
creates for the visitor. The admin token *will* hand back a checkout URL, and that is the trap:
|
|
66
|
+
it belongs to the site rather than the buyer (no cart persistence, no abandoned-checkout
|
|
67
|
+
recovery, no attribution), and a redirect session refuses it outright (`403`). Returns reach
|
|
68
|
+
this app only from URLs in the OAuth app's redirect list (Visitor authentication and
|
|
69
|
+
Wix-hosted flows, below).
|
|
70
|
+
- **No OAuth app yet is not a reason to use the admin token.** `wx.ensureOAuthApp` returns the
|
|
71
|
+
`clientId`, creating the app when the site has none — a visitor flow's first step. Anyone can
|
|
72
|
+
mint an anonymous visitor token from that public `clientId`; no visitor login is required.
|
|
73
|
+
- **No login still means visitors.** `asServiceRole` governs this app's own data access and says
|
|
74
|
+
nothing about which identity Wix sees: a public page's Wix calls run on a visitor token. Reach
|
|
75
|
+
for the admin token because a feature is owner-side, never because nobody signs in.
|
|
76
|
+
- **Owner-side work beyond pages** — webhooks, scheduled jobs, explicitly authorized elevated
|
|
77
|
+
operations — is admin flow too, in backend functions.
|
|
62
78
|
|
|
63
79
|
```
|
|
64
80
|
visitor pages ──(visitor token)────────────────────────► wixapis.com
|
|
@@ -66,6 +82,11 @@ admin pages ──► base44/functions/… ──(admin token)────►
|
|
|
66
82
|
exec_tool ──(admin token, ad hoc management)────────► wixapis.com
|
|
67
83
|
```
|
|
68
84
|
|
|
85
|
+
**The connected site owns its payments.** Whatever the visitor buys is paid on the Wix-hosted page
|
|
86
|
+
this app hands them off to, with the provider the site already has. Base44's
|
|
87
|
+
`<available_payment_providers>` note is about checkout that Base44 hosts instead — it does not
|
|
88
|
+
apply to these flows, and asks nothing of them, whichever providers it names or rules out.
|
|
89
|
+
|
|
69
90
|
## The helpers
|
|
70
91
|
|
|
71
92
|
Research and probing run in exec_tool, and one loader opens every exec (execs share no state —
|
|
@@ -90,6 +111,7 @@ const wx = require(require("path").resolve(P));
|
|
|
90
111
|
- `wx.bash(cmd)` — shell over saved files (GNU grep/sed; awk is mawk; no rg)
|
|
91
112
|
- `wx.spec(docsUrl | code)` — a method's exact schema, plus the titles of the docs' own request examples saved at `examplesPath`; pass a hit's docsUrl (direct load), or raw code to query the index yourself
|
|
92
113
|
- `wx.mgmtRecipes(q?)` — management-recipe index; no arg → categories, a word → matching recipes
|
|
114
|
+
- `wx.ensureOAuthApp(token, { name, redirectUris, redirectDomains })` — the headless OAuth app, found by name or created, with the redirect lists merged into whatever it already allows; returns `{ clientId, created }`. `clientId` is what the browser mints visitor tokens from. Run it whenever `context` reports no OAuth app, or before any Wix-hosted flow that returns to this app — see Visitor authentication and Wix-hosted flows
|
|
93
115
|
- `wx.installApp(appDefId, siteId, token)` — install a Wix app on the site (Apps Installer). If discovery finds an API whose app isn't installed on the site, install it first — that's a one-call prerequisite, **not** a reason to fall back to a hand-built alternative. `appDefId` from `search` or the Apps-Created-by-Wix table; `siteId` from `context` (the site report)
|
|
94
116
|
|
|
95
117
|
Search makes one combined request across REST, management recipes, and Headless by default.
|
|
@@ -114,6 +136,10 @@ One report: installed apps **with ids** (incl. Stores' catalog version — V1 vs
|
|
|
114
136
|
endpoints), the OAuth app id (**also the visitor `clientId`**), locale, currency, CMS collections.
|
|
115
137
|
An empty report = bad token, never an empty site.
|
|
116
138
|
|
|
139
|
+
No OAuth app in the report means the site has none **yet**. For anything a visitor operates,
|
|
140
|
+
create it with `wx.ensureOAuthApp` — one call, returns the `clientId` the frontend mints visitor
|
|
141
|
+
tokens from — and build the feature on the visitor flow.
|
|
142
|
+
|
|
117
143
|
Reports over 4,000 characters are saved in full to a temporary Markdown file. The result includes
|
|
118
144
|
its path, byte and line counts, and a heading outline. Read that file to inspect the site context.
|
|
119
145
|
|
|
@@ -228,7 +254,9 @@ Want to shape the result yourself? Pass raw code against the index (`lightIndex`
|
|
|
228
254
|
await wx.spec(`
|
|
229
255
|
const url = "<docsUrl from search/browse>"; // API method page, not a skill/article page
|
|
230
256
|
const s = await getResourceSchemaByUrl(url);
|
|
231
|
-
|
|
257
|
+
// A method URL resolves to exactly that method — take methods[0]. Don't match on docsUrl:
|
|
258
|
+
// the reader normalizes the url, so equality against the one you passed can miss.
|
|
259
|
+
const m = s.methods[0];
|
|
232
260
|
return {
|
|
233
261
|
call: m.publicUrl, // callable https://www.wixapis.com/… URL
|
|
234
262
|
body: m.requestBody?.content["application/json"].schema.properties,
|
|
@@ -327,13 +355,15 @@ return await wx.post("<a public read from Learn Wix>", { query: {} }, visitorTok
|
|
|
327
355
|
|
|
328
356
|
**Any Wix-hosted flow that returns the visitor to your app needs the headless OAuth app's
|
|
329
357
|
redirect config set.** This covers redirect sessions *and* sending a buyer to the Wix-hosted
|
|
330
|
-
checkout `checkoutUrl` and back — any flow where Wix redirects to a URL on your app.
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
visitors do not need to log in)
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
358
|
+
checkout `checkoutUrl` and back — any flow where Wix redirects to a URL on your app.
|
|
359
|
+
|
|
360
|
+
**Step one of any visitor flow: `wx.ensureOAuthApp` with this app's own URLs.** It returns the
|
|
361
|
+
`clientId` the frontend mints visitor tokens from (anonymous visitors do not need to log in),
|
|
362
|
+
creating the OAuth app when the site has none and adding your URLs to its lists when it already
|
|
363
|
+
has one. Returns only complete for URLs in those lists, and a missing one breaks silently —
|
|
364
|
+
create-checkout and the anonymous token still succeed — until a real buyer is redirected and
|
|
365
|
+
stranded on Wix. It is idempotent by name: never create a second OAuth app for the same Base44
|
|
366
|
+
app, or the first `clientId`'s returns stop working.
|
|
337
367
|
|
|
338
368
|
```js
|
|
339
369
|
// Register BOTH of this app's own URLs — its preview URL and its published URL — so returns work
|
|
@@ -353,20 +383,14 @@ const returnDomains = appOrigins.map(origin => new URL(origin).hostname);
|
|
|
353
383
|
// OAuth redirect configuration: exact login URLs versus domains for other returns.
|
|
354
384
|
// https://dev.wix.com/docs/go-headless/authentication/setup/allow-redirect-uris-and-domains.md
|
|
355
385
|
const { accessToken: adminToken } = await base44.asServiceRole.connectors.getConnection("wix");
|
|
356
|
-
const {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
}, adminToken);
|
|
365
|
-
const clientId = oAuthApp.id; // Public visitor client ID, used by the frontend client above.
|
|
366
|
-
|
|
367
|
-
// If destinations change later, update this OAuth app rather than creating another.
|
|
368
|
-
// Read its existing lists and merge new entries before updating, preserving old entries.
|
|
369
|
-
// https://dev.wix.com/docs/api-reference/business-management/headless/oauth-apps/update-oauth-app.md
|
|
386
|
+
const { clientId, created } = await wx.ensureOAuthApp(adminToken, {
|
|
387
|
+
name: "My App", // this app's name; the lookup key on later runs
|
|
388
|
+
redirectUris: loginCallbacks, // login callbacks: matched exactly by the auth request
|
|
389
|
+
redirectDomains: returnDomains, // returns from Wix-hosted flows: hostnames only
|
|
390
|
+
});
|
|
391
|
+
// clientId is the public visitor client id the frontend client above mints from.
|
|
392
|
+
// When destinations change later — a custom domain, a renamed app — call this again with the
|
|
393
|
+
// new URLs: it updates the same OAuth app and preserves the entries already there.
|
|
370
394
|
|
|
371
395
|
```
|
|
372
396
|
|
|
@@ -353,9 +353,13 @@ async function spec(arg) {
|
|
|
353
353
|
const code = /^async function/.test(s) ? s
|
|
354
354
|
: /^https:\/\/dev\.wix\.com\/docs\//.test(s) ? "async function(){ return await getResourceSchemaByUrl(" + JSON.stringify(s) + "); }"
|
|
355
355
|
: "async function(){ " + s + " }";
|
|
356
|
-
const { result } = await post("https://mcp.wix.com/api/code-mode/search", { code });
|
|
356
|
+
const { result, error } = await post("https://mcp.wix.com/api/code-mode/search", { code });
|
|
357
|
+
// The envelope is 200 even when the query throws — the cause is in `error`, not the status.
|
|
358
|
+
// Surfacing it matters: swallowing it turns "your code threw" into "nothing matched".
|
|
359
|
+
if (error)
|
|
360
|
+
return { error, note: "the query threw — read the message and fix the code; don't re-send it unchanged" };
|
|
357
361
|
if (result == null || (Array.isArray(result) && !result.length))
|
|
358
|
-
return { result, note: "
|
|
362
|
+
return { result, note: "the query ran but matched nothing — widen it, or check the url resolves" };
|
|
359
363
|
const text = JSON.stringify(result, null, 1);
|
|
360
364
|
if (text.length <= BUDGET) return result;
|
|
361
365
|
let h = 5381;
|
|
@@ -424,4 +428,36 @@ async function installApp(appDefId, siteId, token) {
|
|
|
424
428
|
{ tenant: { tenantType: "SITE", id: siteId }, appInstance: { appDefId } }, token);
|
|
425
429
|
}
|
|
426
430
|
|
|
427
|
-
|
|
431
|
+
// The headless OAuth app: the visitor client id, and the redirect config every Wix-hosted
|
|
432
|
+
// return needs. Idempotent by name — a site reached by the "connect an existing site" flow
|
|
433
|
+
// has none, and creating a second one for the same app strands the first app's returns.
|
|
434
|
+
// Redirect lists are merged, never replaced: an app that already serves a custom domain
|
|
435
|
+
// keeps it when a preview URL is added.
|
|
436
|
+
async function ensureOAuthApp(token, { name, redirectUris = [], redirectDomains = [] }) {
|
|
437
|
+
const { oAuthApps = [] } = await post(
|
|
438
|
+
"https://www.wixapis.com/oauth-app/v1/oauth-apps/query", { query: {} }, token);
|
|
439
|
+
const existing = oAuthApps.find((a) => a.name === name);
|
|
440
|
+
const merge = (a = [], b = []) => [...new Set([...a, ...b])];
|
|
441
|
+
if (!existing) {
|
|
442
|
+
const { oAuthApp } = await post("https://www.wixapis.com/oauth-app/v1/oauth-apps", {
|
|
443
|
+
oAuthApp: { name, allowedRedirectUris: redirectUris, allowedRedirectDomains: redirectDomains },
|
|
444
|
+
}, token);
|
|
445
|
+
return { clientId: oAuthApp.id, created: true, oAuthApp };
|
|
446
|
+
}
|
|
447
|
+
const allowedRedirectUris = merge(existing.allowedRedirectUris, redirectUris);
|
|
448
|
+
const allowedRedirectDomains = merge(existing.allowedRedirectDomains, redirectDomains);
|
|
449
|
+
const unchanged =
|
|
450
|
+
allowedRedirectUris.length === (existing.allowedRedirectUris || []).length &&
|
|
451
|
+
allowedRedirectDomains.length === (existing.allowedRedirectDomains || []).length;
|
|
452
|
+
if (unchanged) return { clientId: existing.id, created: false, oAuthApp: existing };
|
|
453
|
+
// The update docs disagree with themselves on the mask field (prose says `paths`, the
|
|
454
|
+
// curl example says `path`); send both so the call does not silently no-op.
|
|
455
|
+
const paths = ["allowedRedirectUris", "allowedRedirectDomains"];
|
|
456
|
+
const { oAuthApp } = await patch(
|
|
457
|
+
`https://www.wixapis.com/oauth-app/v1/oauth-apps/${existing.id}`,
|
|
458
|
+
{ oAuthApp: { id: existing.id, allowedRedirectUris, allowedRedirectDomains },
|
|
459
|
+
mask: { paths, path: paths.join(",") } }, token);
|
|
460
|
+
return { clientId: (oAuthApp || existing).id, created: false, oAuthApp: oAuthApp || existing };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
module.exports = { req, post, get, patch, put, del, clip, context, browse, search, page, bash, spec, mgmtRecipes, installApp, ensureOAuthApp };
|