@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.
Files changed (80) hide show
  1. package/package.json +1 -1
  2. package/skills/wix-app/SKILL.md +33 -31
  3. package/skills/wix-app/references/DASHBOARD_PAGE.md +6 -10
  4. package/skills/wix-app/references/EDITOR_REACT_COMPONENT.md +17 -8
  5. package/skills/wix-app/references/WIX_PATTERNS_DOCS.md +53 -131
  6. package/skills/wix-app/references/dashboard-page/UX_SUCCESS_MODEL.md +1 -2
  7. package/skills/wix-app/references/editor-react-component/ACCESSIBILITY.md +88 -73
  8. package/skills/wix-app/references/editor-react-component/ANIMATED-COMPONENTS.md +46 -22
  9. package/skills/wix-app/references/editor-react-component/COMPONENT-CONTRACT.md +20 -6
  10. package/skills/wix-app/references/editor-react-component/CSS-GUIDELINES.md +9 -0
  11. package/skills/wix-app/references/editor-react-component/FUNCTION-HANDLERS.md +71 -31
  12. package/skills/wix-app/references/editor-react-component/PARTS.md +5 -2
  13. package/skills/wix-app/references/editor-react-component/REACT-GUIDELINES.md +4 -5
  14. package/skills/wix-app/scripts/pkg-root.cjs +73 -0
  15. package/skills/wix-app/scripts/scan-a11y-code.cjs +310 -40
  16. package/skills/wix-app/scripts/scan-a11y-eslint.cjs +118 -24
  17. package/skills/wix-app/scripts/scan-a11y-render.cjs +551 -0
  18. package/skills/wix-app/scripts/scan-a11y-review.cjs +373 -0
  19. package/skills/wix-base44-connector/SKILL.md +67 -43
  20. package/skills/wix-base44-connector/scripts/utils.cjs +39 -3
  21. package/skills/wix-docs/SKILL.md +47 -20
  22. package/skills/wix-headless/SKILL.md +1 -0
  23. package/skills/wix-headless/entry/bootstrap.mjs +106 -50
  24. package/skills/wix-headless/entry/skill.md +5 -3
  25. package/skills/wix-headless/references/CAPABILITIES.md +7 -2
  26. package/skills/wix-headless/references/SDK_HANDOFF.md +2 -1
  27. package/skills/wix-headless/references/inline-recipes/experience-store.md +48 -0
  28. package/skills/wix-headless/references/inline-recipes/how-to-code-a-store.md +350 -114
  29. package/skills/wix-headless/references/inline-recipes/how-to-code-members-astro.md +1 -1
  30. package/skills/wix-headless/references/managed/AUTHENTICATION.md +25 -5
  31. package/skills/wix-headless/references/managed/CREATE.md +5 -0
  32. package/skills/wix-headless-fast/SKILL.md +25 -7
  33. package/skills/wix-headless-fast/entry/skill.md +23 -14
  34. package/skills/wix-headless-fast/install/fast-path.mjs +48 -5
  35. package/skills/wix-headless-fast/install/pin-agents-md.mjs +57 -0
  36. package/skills/wix-headless-fast/references/shared/CUSTOM_OPERATIONS.md +3 -3
  37. package/skills/wix-headless-fast/references/storefront/INSTRUCTIONS.md +204 -35
  38. package/skills/wix-headless-fast/references/storefront/app/hooks/storefront/useShop.ts +146 -36
  39. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/cart-store.ts +8 -2
  40. package/skills/wix-headless-fast/references/storefront/app/wix/storefront/catalog.ts +95 -20
  41. package/skills/wix-headless-fast/references/storefront/seed/SEED.md +21 -2
  42. package/skills/wix-headless-fast/references/storefront/seed/seed-store.mjs +104 -18
  43. package/skills/wix-manage/SKILL.md +16 -10
  44. package/skills/wix-manage/references/bookings/create-and-update-booking-services.md +4 -1
  45. package/skills/wix-manage/references/bookings/create-appointment-service.md +3 -1
  46. package/skills/wix-manage/references/bookings/create-class-service.md +3 -1
  47. package/skills/wix-manage/references/bookings/create-course-service.md +4 -2
  48. package/skills/wix-manage/references/bookings/end-to-end-booking-flow.md +68 -3
  49. package/skills/wix-manage/references/ecommerce/pricing-promotions/ecom-pricing-flow-seasonal-promotion.md +1 -1
  50. package/skills/wix-manage/references/google-ads/billing-and-payment.md +3 -3
  51. package/skills/wix-manage/references/google-ads/create-performance-max-campaign.md +8 -8
  52. package/skills/wix-manage/references/google-ads/get-campaign-suggestions.md +4 -4
  53. package/skills/wix-manage/references/google-ads/google-ads-dashboard-navigation.md +1 -1
  54. package/skills/wix-manage/references/google-ads/install-and-create-account.md +9 -9
  55. package/skills/wix-manage/references/google-ads/manage-campaign-success-guide.md +108 -14
  56. package/skills/wix-manage/references/google-ads/query-campaign-analytics.md +7 -7
  57. package/skills/wix-manage/references/seo/manage-content-plan.md +182 -136
  58. package/skills/wix-manage/references/seo/manage-google-search-console.md +272 -0
  59. package/skills/wix-manage/references/sites/upload-static-site.md +283 -0
  60. package/skills/wix-manage/references/stores/change-store-currency.md +1 -1
  61. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed-with-install.md +3 -1
  62. package/skills/wix-vibe-headless/platforms/base44-ecom-light-seed.md +3 -1
  63. package/skills/wix-vibe-headless/platforms/base44-ecom-light.md +1 -1
  64. package/skills/wix-vibe-headless/references/storefront/INSTRUCTIONS.md +69 -27
  65. package/skills/wix-vibe-headless/references/storefront/app/components/CartDrawer.jsx +32 -14
  66. package/skills/wix-vibe-headless/references/storefront/app/context/CartContext.jsx +14 -3
  67. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductCard.js +41 -35
  68. package/skills/wix-vibe-headless/references/storefront/app/hooks/useProductDetail.js +77 -30
  69. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-cart.js +31 -2
  70. package/skills/wix-vibe-headless/references/storefront/app/rest/wix-store-catalog.js +31 -6
  71. package/skills/wix-app/references/dashboard-page/COLLECTION_TOOLKIT.md +0 -80
  72. package/skills/wix-app/references/dashboard-page/ENTITY_PAGE_TOOLKIT.md +0 -140
  73. package/skills/wix-app/references/dashboard-page/PATTERNS_BUNDLE_READING.md +0 -112
  74. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductCard.tsx +0 -100
  75. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductDetailView.tsx +0 -130
  76. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ProductGrid.tsx +0 -48
  77. package/skills/wix-headless-fast/references/storefront/app/components/storefront/ShopView.tsx +0 -66
  78. package/skills/wix-headless-fast/references/storefront/app/components/storefront/VariantPicker.tsx +0 -103
  79. package/skills/wix-headless-fast/references/storefront/app-astro/pages/products/[slug].astro +0 -56
  80. package/skills/wix-headless-fast/references/storefront/app-astro/pages/shop.astro +0 -23
@@ -0,0 +1,551 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ // oxlint-disable no-console preserve-caught-error no-underscore-dangle
5
+
6
+ /**
7
+ * Render audit for one Editor React Component folder.
8
+ *
9
+ * Renders the component with its default props in plain Node (an SSR check),
10
+ * loads the markup into jsdom together with the component's CSS Modules, and
11
+ * runs axe-core against the rendered root. Prints one JSON line.
12
+ *
13
+ * Spawned by `scan-a11y-review.cjs`; also usable standalone:
14
+ *
15
+ * node <SKILL_ROOT>/scripts/scan-a11y-render.cjs <component-dir>
16
+ */
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const Module = require('module');
21
+ const { createRequire } = require('module');
22
+ const { pathToFileURL } = require('url');
23
+
24
+ const ROOT = process.cwd();
25
+ const LOCAL_REQUIRE = createRequire(__filename);
26
+ const ROOT_REQUIRE = createRequire(path.join(ROOT, 'package.json'));
27
+
28
+ const RCU_SPECIFIER = '@wix/react-component-utils';
29
+ /** Scanner-owned wrapper around the rendered component; axe audits this, never the component's own `id`. */
30
+ const AUDIT_ROOT_ATTR = 'data-a11y-audit-root';
31
+ const COMPONENT_ID = 'component';
32
+ const SOURCE_EXTENSIONS = ['.tsx', '.ts', '.jsx'];
33
+ /** Everything else a relative import points at (svg, png, mp4, lottie, scss, ...) is served as its file name. */
34
+ const LOADABLE_EXTENSIONS = new Set([
35
+ '.js',
36
+ '.cjs',
37
+ '.mjs',
38
+ '.json',
39
+ '.node',
40
+ '.css',
41
+ ...SOURCE_EXTENSIONS,
42
+ ]);
43
+ const HTML_SNIPPET_LENGTH = 200;
44
+ const PLACEHOLDER_TEXT = 'No content to display yet';
45
+ /** Test hook: comma-separated tooling packages to treat as missing. */
46
+ const SIMULATE_MISSING = new Set(
47
+ (process.env.A11Y_SIMULATE_MISSING || '').split(',').filter(Boolean),
48
+ );
49
+
50
+ /** Rules that only make sense for a whole page, never for a component fragment. */
51
+ const PAGE_RULES = [
52
+ 'region',
53
+ 'landmark-one-main',
54
+ 'landmark-banner-is-top-level',
55
+ 'landmark-complementary-is-top-level',
56
+ 'landmark-contentinfo-is-top-level',
57
+ 'landmark-main-is-top-level',
58
+ 'landmark-no-duplicate-banner',
59
+ 'landmark-no-duplicate-contentinfo',
60
+ 'landmark-no-duplicate-main',
61
+ 'landmark-unique',
62
+ 'page-has-heading-one',
63
+ 'document-title',
64
+ 'html-has-lang',
65
+ 'html-lang-valid',
66
+ 'html-xml-lang-mismatch',
67
+ 'bypass',
68
+ 'meta-viewport',
69
+ 'meta-viewport-large',
70
+ 'meta-refresh',
71
+ 'meta-refresh-no-exceptions',
72
+ // `frame-title` stays on: an unnamed iframe inside a component is the component's defect.
73
+ 'frame-title-unique',
74
+ 'frame-tested',
75
+ 'frame-focusable-content',
76
+ // Deprecated in axe 4.x and noisy; `duplicate-id-aria` stays on.
77
+ 'duplicate-id',
78
+ 'duplicate-id-active',
79
+ ];
80
+
81
+ /** Rules that need a layout engine. Disabled on jsdom; a browser engine would enable them. */
82
+ const LAYOUT_RULES = [
83
+ 'color-contrast',
84
+ 'color-contrast-enhanced',
85
+ 'target-size',
86
+ 'scrollable-region-focusable',
87
+ 'link-in-text-block',
88
+ 'p-as-heading',
89
+ ];
90
+
91
+ const NOT_CHECKED_BY_JSDOM = [
92
+ ...LAYOUT_RULES,
93
+ '--display-driven visibility',
94
+ 'keyboard interaction',
95
+ ];
96
+
97
+ const SITE_URL = 'https://example.wixsite.com/site';
98
+ const HOME_PAGE = { id: 'home', title: 'Home', path: '/', popup: false };
99
+
100
+ /** Plain-object stand-ins for the site services `@wix/react-component-utils` hooks read. */
101
+ const SERVICES = {
102
+ // `useIsEditMode()` returns `!previewMode`; audit live-equivalent output.
103
+ '@wix/site-service-editor-context': { previewMode: true },
104
+ '@wix/site-service-device-info': { deviceType: 'Desktop', reducedMotion: false },
105
+ '@wix/site-service-locale': { language: 'en', direction: 'ltr' },
106
+ '@wix/site-service-pages': {
107
+ pages: { [HOME_PAGE.id]: { title: HOME_PAGE.title, path: HOME_PAGE.path, popup: false } },
108
+ mainPage: HOME_PAGE,
109
+ currentPage: HOME_PAGE,
110
+ },
111
+ '@wix/site-service-url': {
112
+ currentUrl: SITE_URL,
113
+ siteUrl: SITE_URL,
114
+ pages: [],
115
+ pageIdToPrefix: {},
116
+ },
117
+ };
118
+
119
+ const IMPACT_SEVERITY = { critical: 'high', serious: 'high', moderate: 'medium', minor: 'low' };
120
+
121
+ // ─────────────────────────────────────────────────────────────────────────────
122
+ // Dependencies and entry discovery
123
+ // ─────────────────────────────────────────────────────────────────────────────
124
+
125
+ /** Tooling resolves from the consumer project first, then next to this script. */
126
+ function loadTooling(names) {
127
+ const modules = {};
128
+ const missing = [];
129
+ for (const name of names) {
130
+ if (SIMULATE_MISSING.has(name)) {
131
+ missing.push(name);
132
+ continue;
133
+ }
134
+ try {
135
+ modules[name] = ROOT_REQUIRE(name);
136
+ } catch {
137
+ try {
138
+ modules[name] = LOCAL_REQUIRE(name);
139
+ } catch {
140
+ missing.push(name);
141
+ }
142
+ }
143
+ }
144
+ return { modules, missing };
145
+ }
146
+
147
+ function listFiles(dir, predicate, acc = []) {
148
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
149
+ const full = path.join(dir, entry.name);
150
+ if (entry.isDirectory()) {
151
+ if (entry.name !== 'node_modules') listFiles(full, predicate, acc);
152
+ } else if (entry.isFile() && predicate(entry.name)) {
153
+ acc.push(full);
154
+ }
155
+ }
156
+ return acc;
157
+ }
158
+
159
+ function resolveEntry(componentDir) {
160
+ const basename = path.basename(componentDir);
161
+ const existing = (name) =>
162
+ fs.existsSync(path.join(componentDir, name)) ? path.join(componentDir, name) : null;
163
+ const tsxFiles = listFiles(componentDir, (name) => name.endsWith('.tsx'));
164
+ const impl =
165
+ existing(`${basename}.tsx`) ||
166
+ tsxFiles.find(
167
+ (file) =>
168
+ path.dirname(file) === componentDir && !path.basename(file).startsWith('component.'),
169
+ ) ||
170
+ null;
171
+
172
+ return {
173
+ live: existing('component.tsx'),
174
+ preview: existing('component.preview.tsx'),
175
+ impl,
176
+ props: existing(`${basename}.props.ts`),
177
+ cssFiles: listFiles(componentDir, (name) => name.endsWith('.module.css')),
178
+ };
179
+ }
180
+
181
+ // ─────────────────────────────────────────────────────────────────────────────
182
+ // Compile hooks and runtime
183
+ // ─────────────────────────────────────────────────────────────────────────────
184
+
185
+ function installCompileHooks(ts) {
186
+ const compile = (module, filename) => {
187
+ const { outputText } = ts.transpileModule(fs.readFileSync(filename, 'utf8'), {
188
+ fileName: filename,
189
+ compilerOptions: {
190
+ module: ts.ModuleKind.CommonJS,
191
+ target: ts.ScriptTarget.ES2020,
192
+ jsx: ts.JsxEmit.ReactJSX,
193
+ esModuleInterop: true,
194
+ allowJs: true,
195
+ isolatedModules: true,
196
+ },
197
+ });
198
+ module._compile(outputText, filename);
199
+ };
200
+ for (const ext of SOURCE_EXTENSIONS) require.extensions[ext] = compile;
201
+
202
+ // CSS Modules map every class to its own name so the markup keeps the
203
+ // module class names the component CSS targets.
204
+ const cssProxy = new Proxy(
205
+ {},
206
+ {
207
+ get(_target, prop) {
208
+ if (prop === '__esModule') return true;
209
+ if (prop === 'default') return cssProxy;
210
+ return typeof prop === 'symbol' ? undefined : String(prop);
211
+ },
212
+ },
213
+ );
214
+ require.extensions['.css'] = (module) => {
215
+ module.exports = cssProxy;
216
+ };
217
+ }
218
+
219
+ /**
220
+ * Load React and `@wix/react-component-utils` from the component's own
221
+ * dependency tree so hooks and context share one React instance. The utils
222
+ * package is ESM-only, so it is imported once and served to the transpiled
223
+ * CommonJS component code through `Module._load`.
224
+ */
225
+ async function loadRuntime(entryFile) {
226
+ const entryRequire = createRequire(entryFile);
227
+ const React = entryRequire('react');
228
+ const { renderToStaticMarkup } = entryRequire('react-dom/server');
229
+
230
+ const rcuDir = (entryRequire.resolve.paths(RCU_SPECIFIER) || [])
231
+ .map((dir) => path.join(dir, RCU_SPECIFIER))
232
+ .find((dir) => fs.existsSync(path.join(dir, 'package.json')));
233
+ if (!rcuDir) {
234
+ throw new Error(`Cannot resolve ${RCU_SPECIFIER} from ${path.relative(ROOT, entryFile)}.`);
235
+ }
236
+
237
+ const manifest = JSON.parse(fs.readFileSync(path.join(rcuDir, 'package.json'), 'utf8'));
238
+ const root = manifest.exports && manifest.exports['.'];
239
+ const entry =
240
+ (typeof root === 'string' ? root : root && (root.import || root.default)) || manifest.main;
241
+ const rcu = { __esModule: true, ...(await import(pathToFileURL(path.join(rcuDir, entry)).href)) };
242
+
243
+ const originalLoad = Module._load;
244
+ Module._load = function patchedLoad(request, parent, ...rest) {
245
+ if (request === RCU_SPECIFIER) return rcu;
246
+ const query = request.indexOf('?');
247
+ const bare = query === -1 ? request : request.slice(0, query);
248
+ // Packages load as-is; only the component's own relative imports get the asset treatment.
249
+ if (query === -1 && !/^[./]/.test(bare))
250
+ return originalLoad.call(this, request, parent, ...rest);
251
+ // Throws MODULE_NOT_FOUND for a missing file, which the audit reports as a loader limit.
252
+ const file = Module._resolveFilename(bare, parent);
253
+ if (query === -1 && LOADABLE_EXTENSIONS.has(path.extname(file).toLowerCase())) {
254
+ return originalLoad.call(this, request, parent, ...rest);
255
+ }
256
+ // Vite resource queries: `?raw` is the file text, `?url`/`?inline` its name. A bare asset
257
+ // import (mp4, lottie, scss, ...) is its name too; Node would otherwise parse it as JS.
258
+ const raw = query !== -1 && /(^|&)raw(&|$)/.test(request.slice(query + 1));
259
+ return { __esModule: true, default: raw ? fs.readFileSync(file, 'utf8') : path.basename(file) };
260
+ };
261
+
262
+ if (!globalThis.WixReactContext) globalThis.WixReactContext = React.createContext(undefined);
263
+ return { React, renderToStaticMarkup, rcu, WixContext: globalThis.WixReactContext };
264
+ }
265
+
266
+ function createServicesProvider(React, WixContext) {
267
+ const manager = {
268
+ getService(definition) {
269
+ const id = String(definition);
270
+ if (!(id in SERVICES)) {
271
+ throw new Error(`Unknown site service "${id}" requested during render.`);
272
+ }
273
+ return SERVICES[id];
274
+ },
275
+ hasService(definition) {
276
+ return String(definition) in SERVICES;
277
+ },
278
+ };
279
+ return ({ children }) => React.createElement(WixContext.Provider, { value: manager }, children);
280
+ }
281
+
282
+ const interopDefault = (loaded) =>
283
+ loaded && loaded.__esModule && loaded.default !== undefined ? loaded.default : loaded;
284
+
285
+ function loadComponent(entry, runtime) {
286
+ if (entry.live) {
287
+ return { Component: interopDefault(require(entry.live)), source: 'component.tsx' };
288
+ }
289
+ if (!entry.impl) throw new Error('No component.tsx or implementation .tsx found.');
290
+ const Impl = interopDefault(require(entry.impl));
291
+ const defaultProps = entry.props ? require(entry.props).defaultProps || {} : {};
292
+ return {
293
+ Component: runtime.rcu.withDefaults(Impl, defaultProps),
294
+ source: path.basename(entry.impl),
295
+ };
296
+ }
297
+
298
+ /** The wrapper is the audit boundary, so a component that forwards `id` to an inner element still gets all of its output audited. */
299
+ function render(runtime, Provider, Component) {
300
+ const { React, renderToStaticMarkup } = runtime;
301
+ return renderToStaticMarkup(
302
+ React.createElement(
303
+ Provider,
304
+ null,
305
+ React.createElement(
306
+ 'div',
307
+ { [AUDIT_ROOT_ATTR]: '' },
308
+ React.createElement(Component, { id: COMPONENT_ID }),
309
+ ),
310
+ ),
311
+ );
312
+ }
313
+
314
+ const LOADER_ERROR_CODES = new Set([
315
+ 'MODULE_NOT_FOUND',
316
+ 'ERR_REQUIRE_ESM',
317
+ 'ERR_REQUIRE_ASYNC_MODULE',
318
+ 'ERR_UNKNOWN_FILE_EXTENSION',
319
+ ]);
320
+
321
+ /** The component could not be loaded at all: a scanner limitation, never a finding. */
322
+ const isLoaderError = (error) =>
323
+ Boolean(error) &&
324
+ (LOADER_ERROR_CODES.has(error.code) || /Cannot find module/.test(error.message || ''));
325
+
326
+ function classifyRenderError(error) {
327
+ const message = error && error.message ? error.message : String(error);
328
+ if (/No ServiceManagerProvider|Unknown site service/.test(message)) {
329
+ return {
330
+ rule: 'render-unsupported-hook',
331
+ severity: 'low',
332
+ confidence: 'low',
333
+ message: `The component reads a site service the audit cannot provide: ${message}`,
334
+ };
335
+ }
336
+ const browserGlobal =
337
+ /\b(window|document|navigator|localStorage|sessionStorage|matchMedia)\b.*is not defined/.test(
338
+ message,
339
+ );
340
+ return {
341
+ rule: 'render-failed',
342
+ severity: 'high',
343
+ confidence: 'high',
344
+ message: browserGlobal
345
+ ? `The first render touches a browser global: ${message}`
346
+ : `Rendering with defaultProps threw: ${message}`,
347
+ };
348
+ }
349
+
350
+ const checkPreviewPlaceholder = (html) =>
351
+ html.includes(PLACEHOLDER_TEXT) || /class="[^"]*_placeholder_/.test(html);
352
+
353
+ // ─────────────────────────────────────────────────────────────────────────────
354
+ // jsdom + axe
355
+ // ─────────────────────────────────────────────────────────────────────────────
356
+
357
+ /** Strip CSS Modules syntax jsdom cannot parse; keep every declaration. */
358
+ function normalizeModuleCss(css) {
359
+ return css
360
+ .replace(/@value[^;]*;/g, '')
361
+ .replace(/\bcomposes\s*:[^;{}]*;/g, '')
362
+ .replace(/:global\(([^)]*)\)/g, '$1')
363
+ .replace(/:local\(([^)]*)\)/g, '$1')
364
+ .replace(/:global\s+/g, '');
365
+ }
366
+
367
+ function truncate(text, length) {
368
+ const compact = String(text || '').replace(/\s+/g, ' ');
369
+ return compact.length > length ? `${compact.slice(0, length - 1)}…` : compact;
370
+ }
371
+
372
+ async function runAxe(jsdom, axe, html, css) {
373
+ const dom = new jsdom.JSDOM(
374
+ `<!doctype html><html lang="en"><head><style>${css}</style></head><body>${html}</body></html>`,
375
+ { virtualConsole: new jsdom.VirtualConsole() },
376
+ );
377
+
378
+ const { window } = dom;
379
+ const previous = { window: globalThis.window, document: globalThis.document };
380
+ globalThis.window = window;
381
+ globalThis.document = window.document;
382
+ try {
383
+ const known = new Set(axe.getRules().map((rule) => rule.ruleId));
384
+ const rules = {};
385
+ for (const ruleId of [...PAGE_RULES, ...LAYOUT_RULES]) {
386
+ if (known.has(ruleId)) rules[ruleId] = { enabled: false };
387
+ }
388
+ const root = window.document.querySelector(`[${AUDIT_ROOT_ATTR}]`) || window.document.body;
389
+ const results = await axe.run(root, { rules, resultTypes: ['violations'], elementRef: false });
390
+
391
+ const findings = [];
392
+ for (const violation of results.violations) {
393
+ for (const node of violation.nodes) {
394
+ findings.push({
395
+ rule: violation.id,
396
+ source: 'render',
397
+ severity: IMPACT_SEVERITY[violation.impact] || 'medium',
398
+ confidence: 'high',
399
+ message: violation.help,
400
+ detail: truncate(node.failureSummary, 240),
401
+ target: node.target.join(' '),
402
+ html: truncate(node.html, HTML_SNIPPET_LENGTH),
403
+ helpUrl: violation.helpUrl,
404
+ });
405
+ }
406
+ }
407
+ return { findings, rulesRun: known.size - Object.keys(rules).length };
408
+ } finally {
409
+ globalThis.window = previous.window;
410
+ globalThis.document = previous.document;
411
+ if (previous.window === undefined) delete globalThis.window;
412
+ if (previous.document === undefined) delete globalThis.document;
413
+ }
414
+ }
415
+
416
+ // ─────────────────────────────────────────────────────────────────────────────
417
+ // Main
418
+ // ─────────────────────────────────────────────────────────────────────────────
419
+
420
+ async function audit(componentDir) {
421
+ const report = {
422
+ ok: false,
423
+ engine: 'jsdom',
424
+ componentDir: path.relative(ROOT, componentDir) || '.',
425
+ entry: null,
426
+ preview: null,
427
+ ssr: null,
428
+ axe: null,
429
+ findings: [],
430
+ notChecked: [...NOT_CHECKED_BY_JSDOM],
431
+ };
432
+
433
+ if (!fs.existsSync(componentDir) || !fs.statSync(componentDir).isDirectory()) {
434
+ report.error = `Component directory not found: ${componentDir}`;
435
+ return report;
436
+ }
437
+
438
+ const { modules, missing } = loadTooling(['typescript', 'jsdom', 'axe-core']);
439
+ if (missing.length > 0) {
440
+ report.reason = 'missing-deps';
441
+ report.missing = missing;
442
+ report.error = `Missing render dependencies: ${missing.join(', ')}. Install them (SKILL.md step 2) and rerun.`;
443
+ return report;
444
+ }
445
+
446
+ const entry = resolveEntry(componentDir);
447
+ if (!entry.live && !entry.impl) {
448
+ report.error = 'No component.tsx or implementation .tsx found.';
449
+ return report;
450
+ }
451
+
452
+ installCompileHooks(modules.typescript);
453
+ const runtime = await loadRuntime(entry.live || entry.impl);
454
+ const Provider = createServicesProvider(runtime.React, runtime.WixContext);
455
+
456
+ // Live render: the markup visitors get.
457
+ let liveHtml = null;
458
+ try {
459
+ const { Component, source } = loadComponent(entry, runtime);
460
+ report.entry = source;
461
+ liveHtml = render(runtime, Provider, Component);
462
+ report.ssr = { ok: true, bytes: liveHtml.length };
463
+ } catch (error) {
464
+ if (isLoaderError(error)) {
465
+ report.reason = 'loader';
466
+ report.error = truncate(error.stack || error.message, 600);
467
+ return report;
468
+ }
469
+ report.ssr = { ok: false, error: truncate(error && error.stack ? error.stack : error, 600) };
470
+ report.findings.push({ ...classifyRenderError(error), source: 'render' });
471
+ }
472
+
473
+ // Preview render: must not fall back to the placeholder with default props.
474
+ if (entry.preview) {
475
+ try {
476
+ const Preview = interopDefault(require(entry.preview));
477
+ const placeholder = checkPreviewPlaceholder(render(runtime, Provider, Preview));
478
+ report.preview = placeholder ? 'placeholder' : 'ok';
479
+ if (placeholder) {
480
+ report.findings.push({
481
+ rule: 'preview-placeholder',
482
+ source: 'render',
483
+ severity: 'high',
484
+ confidence: 'high',
485
+ message:
486
+ 'component.preview.tsx renders the fallback placeholder with defaultProps, so the editor shows an empty box.',
487
+ });
488
+ }
489
+ } catch (error) {
490
+ if (isLoaderError(error)) {
491
+ report.reason = 'loader';
492
+ report.error = truncate(error.stack || error.message, 600);
493
+ return report;
494
+ }
495
+ report.preview = 'failed';
496
+ report.findings.push({
497
+ rule: 'preview-render-failed',
498
+ source: 'render',
499
+ severity: 'medium',
500
+ confidence: 'high',
501
+ message: `component.preview.tsx threw with defaultProps: ${truncate(error && error.message, 200)}`,
502
+ });
503
+ }
504
+ }
505
+
506
+ if (liveHtml !== null) {
507
+ const css = entry.cssFiles
508
+ .map((file) => normalizeModuleCss(fs.readFileSync(file, 'utf8')))
509
+ .join('\n');
510
+ const result = await runAxe(modules.jsdom, modules['axe-core'], liveHtml, css);
511
+ report.axe = { rulesRun: result.rulesRun };
512
+ report.findings.push(...result.findings);
513
+ }
514
+
515
+ report.ok = true;
516
+ return report;
517
+ }
518
+
519
+ async function main() {
520
+ const componentDir = process.argv.slice(2).find((arg) => !arg.startsWith('--'));
521
+ if (!componentDir) {
522
+ console.log(
523
+ JSON.stringify({
524
+ ok: false,
525
+ usage: 'node <SKILL_ROOT>/scripts/scan-a11y-render.cjs <component-dir>',
526
+ }),
527
+ );
528
+ process.exit(2);
529
+ }
530
+ try {
531
+ console.log(JSON.stringify(await audit(path.resolve(componentDir))));
532
+ } catch (error) {
533
+ console.log(
534
+ JSON.stringify({
535
+ ok: false,
536
+ error: truncate(error && error.stack ? error.stack : error, 800),
537
+ }),
538
+ );
539
+ process.exit(2);
540
+ }
541
+ }
542
+
543
+ module.exports = {
544
+ PAGE_RULES,
545
+ LAYOUT_RULES,
546
+ normalizeModuleCss,
547
+ classifyRenderError,
548
+ checkPreviewPlaceholder,
549
+ };
550
+
551
+ if (require.main === module) main();