AutoGineer summary
Written by AutoGineer from the skill’s own files, not by the author
The wix-app skill provides a structured, mandatory workflow for developers building extensions for Wix applications. It covers the entire lifecycle from scaffolding via the Wix CLI to implementing business logic, UI components, and backend integrations. The skill enforces strict architectural patterns, such as the SDK-First rule for data management and specific component selection orders for dashboard UIs, ensuring that extensions are built according to Wix's technical standards and are ready for App Market review.
What it does
- Guides the scaffolding of various extension types including dashboard pages, backend APIs, and site plugins.
- Enforces a mandatory checklist for extension development, including SDK-first data handling and UI component selection.
- Provides decision logic for choosing the correct extension type based on user requirements and visibility.
- Facilitates App Market readiness by mapping code and configuration against official review requirements.
- Manages the integration of Wix patterns and design systems to ensure consistent dashboard experiences.
- Validates project configuration, dependencies, and build status before completion.
When to use it
- When starting a new feature or extension for a Wix CLI application.
- When you need to determine the correct extension type for a specific business requirement.
- When building dashboard UIs that require adherence to Wix design and pattern standards.
- When preparing an application for App Market submission and needing to audit against review guidelines.
- When integrating backend logic, events, or data collections into a Wix app.
Advantages
- Ensures architectural consistency across all app extensions.
- Prevents common pitfalls like incorrect data source usage or improper UI component selection.
- Provides a clear, step-by-step path to App Market submission readiness.
- Reduces development errors by enforcing build-time validation and compilation checks.
- Standardizes the use of Wix SDKs and patterns for better maintainability.
Usage examples
- “Help me build a dashboard page that displays a list of custom orders.”
- The agent will guide you through selecting the appropriate dashboard page template, implementing the required data fetching logic using the correct SDK, and applying the necessary Business Manager themes.
- “I need to add a backend API endpoint to my Wix app.”
- The agent will provide the steps to scaffold an HTTP_ENDPOINT, explain the registration process, and ensure the business logic follows the required code quality standards.
- “Is my app ready for the Wix App Market?”
- The agent will initiate the App Market review workflow, auditing your current configuration and code against the official requirements and identifying any missing components or blockers.
- “How do I create a data collection for my app's configuration?”
- The agent will verify if the data is app-owned, help you obtain the necessary namespace, and guide you through the DATA_COLLECTION scaffolding process.
Install
Download the skill as a ZIP:
https://mp-staging.autogineer.com/agent-skills/wix/wix-app/downloadUnzip it into the cross-client skills folder of your project:
mkdir -p .agents/skills && unzip wix-app.zip -d .agents/skills
Documentation by Wix
Wix App Builder
Helps build extensions for Wix CLI applications. Covers all extension types: dashboard pages, modals, plugins, menu plugins, custom element widgets, Editor React components, site plugins, embedded scripts, context providers, function libraries, backend APIs, events, service plugins, and data collections.
Scaffolding is owned by the Wix CLI. Use wix generate --params for every supported type. It generates files and, where applicable, builder boilerplate, UUIDs, and src/extensions.ts registration. HTTP endpoints are discovered from files and need no registration. This skill provides the decision logic, API guidance, configuration semantics, and business-logic patterns that fill in the generated stubs.
⚠️ MANDATORY WORKFLOW CHECKLIST ⚠️
Before reporting completion to the user, ALL boxes MUST be checked:
[ ] Step 1: Determined extension type(s) needed
[ ] Asked clarifying questions if requirements were unclear
[ ] 🛑 SDK-First Gate (MANDATORY before any Data Collection): Confirmed the data is NOT owned by an existing Wix app — if it is, use its SDK module, never CMS (see SDK-First Rule)
[ ] Checked for implicit Data Collection need — unless user provided a collection ID directly (see Data Collection Inference)
[ ] Obtained app namespace if Data Collection extension is being created
[ ] Determined full scoped collection IDs if Data Collection extension is being created (see Collection ID Coordination)
[ ] Explained recommendation with reasoning
[ ] Step 2: Read extension reference file(s) for the chosen type(s) and the project-wide CODE_QUALITY.md
[ ] [ ] Dashboard page UI: Translated the prompt into a workflow before choosing components — what the user must understand, focus on, investigate, act on, and see confirmed. See UX Success Model, and the installed package's own
Collection Toolkit.mdguide for which component serves each need (The Discovery Chain).[ ] No
SummaryBarunless the request asked for one — a named total, count, or "how many / how much" figure in the prompt. Not "the page seems like it wants one": an uninvited bar pushes the rows down and puts a number on screen nobody asked to be right about. When the request did ask, the number has to come from something that counts (QUERY_AND_PAGING.md) and be read throughuseSelector, because the state is MobX (TABLE_STATE.md).[ ] A row the user can open, as a page — editable by default —
navigateToEntityPageto anEntityPagewith a working form and save, for CMS and vertical SDK data alike; the three read-only cases are in DRAFT_TEMPLATE.md. Unless the prompt is explicitly a report or an export. Never aSidePanel: in Cairo that hosts a page's own panels.[ ] Every filter reaches the query: declared in the collection hook's
filtersand read insidefetchData. Filter UI that never narrows the rows is a defect that looks like a feature.
A filtered table with no drill-in and no working filters is what gets built when nobody states the requirement — the most common way a generated dashboard disappoints. The aggregate is the judgment call; the drill-in and the filters are not.
[ ] [ ] 🛑 Template-First Gate (MANDATORY, dashboard UI only, comes before writing any shell/provider/router): Followed DRAFT_TEMPLATE.md: found the page templates the installed
@wix/patternsships, chose one with itsPage Templates.mdguide — which pages the workflow above needs, and where the rows come from (your own fetch, or a CMS collection) — read that template's page, and copied every file--templateslists for it. Composing the page shell, provider nesting, or router wiring from scratch when a template already shows it is the failure mode this gate exists to prevent — the Patterns/Component Docs gates below are for what the template doesn't cover, not a replacement for starting there. The reverse holds too: a template replaces composing the shell, never the discovery chain, so every symbol it doesn't show still goes through the next gate.[ ] [ ] 🛑 Patterns Docs Gate (MANDATORY for any dashboard page UI): Read WIX_PATTERNS_DOCS.md before the first command that touches
node_modules/@wix/patterns— its discovery chain (pkg-root.cjs, the index,Composition and Providers.mdonce,Collection Toolkit.mdfor which component serves a need) lives only in that file, and this line is a summary of its step 1, not a substitute for opening it. Then resolve every patterns name you plan to write in onenode <pkgRoot>/bin/patterns-lookup.cjs <name…>call: per name, it prints the import, the examples and the one props file. Upgrade@wix/patternsifdist/docs/index.jsonis missing. Patterns API facts come only from the publisheddist/docs/(pages),dist/examples/(worked calls),dist/dts-bundle/(types) anddist/templates/(whole pages) trees — never fromsrc/,dist/esm/,dist/types/, or any other path inside the package. A prop a bundle has stubbed is in the file its stub names (Reading the Doc Indices.md).[ ] [ ] 🛑 Component Docs Gate (MANDATORY, dashboard UI only): For each patterns symbol you are about to write, decided from the lookup output which single artifact answers the question you actually have — the import line, an example, or the props file — and read only that one. Procedure: COMPONENT_SELECTION.md. Resolve every patterns name in one
node <pkgRoot>/bin/patterns-lookup.cjs <name…>call, not one probe per name. State which artifact you read per symbol, and why, before the first line of JSX. Reading a doc and its bundle for the same symbol, or opening a page for an import line the lookup already gave you, is the failure this gate exists to prevent.For the object
useTableCollection()returns, read TABLE_STATE.md — a state object you receive rather than construct, whose members are unobvious and several plausible ones absent.
[ ] Step 3: Checked API references; resolved every vertical entity in one
sdk-lookup.cjscall (SDK lookup); used the docs only for what it does not print[ ] Dashboard page over Wix data: took the method from the lookup and verified every mapped field against the declaration it points to (
type … file:line) — see DATA_SOURCES.md, and QUERY_AND_PAGING.md before writingfetchData. A field marked@deprecatedstill compiles and renders something plausible and wrong.[ ] Vertical SDK prerequisites — for every
@wix/*vertical the page touches, including one added later: confirmed the package is actually a dependency (installed it if not), and noted the Dev Center permission scope the read needs — a missing scope produces a page that builds, mounts and shows nothing. Both in DATA_SOURCES.md; the scope goes under Manual Steps Required. A second vertical added during Step 4b needs this check too, and its failure must not take down the page — see A second vertical is a second scope.[ ] Modelled the call on the SDK, not the REST page: namespace name,
_idvsid, noReturnTypeon overloaded methods, nohasNextonPagingMetadataV2— see The SDK is not the REST API.[ ] Site/editor extensions only: kept SDK calls in the extension by default, routing out only business-wide methods a visitor genuinely cannot call (see Identity and Elevation Requirement)
[ ] Step 4a: Scaffolded each CLI-supported extension via
wix generate --params[ ] Step 4b: Filled in business logic in the generated files
[ ] Extension metadata (events, service plugins, data collections, dashboard pages, modals, dashboard plugins, dashboard menu plugins): added the
nameandaiContexteach scaffolded builder declares — see EXTENSION_METADATA.md.[ ] Compile as you go: ran
npx tsc --noEmitafter the first file that imports@wix/patterns, not only at Step 5. Patterns' state and filter APIs are the most common source of errors, and finding twenty of them in one batch after the page is written costs far more than finding two early.[ ] 🛑 Component Selection Gate (MANDATORY, dashboard UI only): For every UI element on a Dashboard Page, resolved it against
@wix/patternsBEFORE reaching for@wix/design-system— and never hand-rolled a component either library already provides. See Component Selection Order.[ ] Invoked
wix-design-systemskill ONLY before editing the first.tsx/.jsxfile that imports@wix/design-system. Skip for backend-only or data-only extensions.[ ] Images: display → IMAGE_RENDERING.md; editable entity fields → IMAGE_EDITING.md.
[ ] WDS: the design-system stylesheets are imported in exactly one place per app —
BusinessManagerTheme.tsxfor a dashboard surface (see the next item), or the main component entry file for a site/editor extension. Never in child, tab or helper files, and never twice.[ ] 🛑 Business Manager theme (every dashboard surface — page, modal AND plugin): wrote
BusinessManagerTheme.tsxonce (both stylesheets, includingthemes/odeditor.global.css, plusWixDesignSystemProvider→WixDesignSystemIconThemeProvider→IconThemeProvider theme="odeditor"→WixDesignSystemDefaultPropsProvider), and wrapped each extension's root in it — aboveWixPatternsProviderand aboveCustomModalLayout. Any extension that rendersWixPatternsProvideralso getsBusinessManagerPatternsTheme.tsx, written once per app, nested directly inside the theme wrapper and aboveWixPatternsProvider. Patterns switches its own Harmony layout on thebmHarmonyThemeexperiment, which never reaches the iframe. Without the second wrapper, the toolbar keeps an outlined Filter button and a pill search on an otherwise themed page (BUSINESS_MANAGER_PATTERNS.md). Every extension is a separate iframe that inherits none of the redesign, so theming the page does nothing for a modal it opens or a plugin in a slot; each one needs its own wrapper. Also: every icon from@wix/wix-ui-icons-common/lazy, and--wds-*tokens orskin/sizeprops rather than hardcoded colours, font sizes or inlinestyle(BUSINESS_MANAGER_TOKENS.md — note the theme rebases theSP*spacing unit from 6px to 4px).tsc,wix buildandwix previewall pass on an unthemed surface — only looking at it catches this. See BUSINESS_MANAGER_THEME.md.
[ ] Step 4c (dashboard page UI only): Re-opened and read the page file(s) just written — not recalled intent — and confirmed against the actual code: no
SummaryBarunless the request asked for one, a routed drill-in (navigateToEntityPage) for every row and noSidePanelused as one, every declared filter name also appearing insidefetchData, and — for every template with a router — the entry file both passes and guardslocation. See UX Completeness Self-Audit.[ ] Step 5: Ran validation (see Validation)
[ ] Dependencies installed
[ ] TypeScript compiled
[ ] Build succeeded
[ ] Preview deployed
[ ] Step 6: Collected and presented ALL manual action items to user
🛑 STOP: If any box is unchecked, do NOT proceed to the next step.
Quick Decision Helper
What are you trying to build?
Admin interface → Dashboard Extensions
Backend logic → Backend Extensions
Data storage / CMS collections → Data Collection (app-owned data only — see SDK-First Rule)
Editor React component → Site Extensions (app projects only)
Who will see it?
Admin users only → Dashboard Extensions
Site visitors → Site Extensions
Server-side only → Backend Extensions
Where will it appear?
Dashboard sidebar/page →
Full admin screen: Dashboard Page — UI built with
@wix/patterns+@wix/design-system(see Component Selection Order). Cannot use<Modal />— use a separate Dashboard Modal extension anddashboard.openModal()instead.Popup/form: Dashboard Modal
Existing Wix app dashboard (widget) → Dashboard Plugin
Existing Wix app dashboard (menu item, more-actions/bulk-actions menu) → Dashboard Menu Plugin
Anywhere on site, standalone → custom element widget
Anywhere on site, with editor manifest (styling/content/elements) → Editor React component
Logic or shared state attached to an element already on the page (named, not asked to be created), or exposed to Editor Binding → Context Provider (wins over Editor React component when the element already exists)
Code that runs once when one named page renders (page view event, UTM capture, A/B exposure) → Page onLoad (a context provider with no API); on every or any page, including future ones → Embedded Script
Reusable functions the editor can call (formatters, helpers, event handlers) → Function Library
Code that opens, closes or passes data to a popup → Lightbox rules, applied to the extension that renders on the site (usually a context provider)
Fixed slot on a Wix business solution page → Site Plugin
Scripts/analytics only, no UI → Embedded Script
During business flow (checkout/shipping/tax) → Service Plugin
Exposing tools to the Wix AI assistant → App Tools (requires both
APP_TOOLSdeclaration +TOOLS_PROVIDER_CONFIGhandler — see APP_TOOLS.md)After event occurs (webhooks/sync) → Backend Event Extension
Custom HTTP endpoint → Backend API
Component Selection Order
Dashboard UI comes from two libraries, resolved in this order. Read COMPONENT_SELECTION.md before choosing any component.
@wix/patterns: the page shell and everything collection-shaped (tables, filters, search, row and bulk actions, overlays, the page for one item). Look it up withpatterns-lookup.cjs(WIX_PATTERNS_DOCS.md); never assemble it from WDS parts.@wix/design-system: leaf-level UI patterns does not own (inputs, buttons, form fields, text, layout, cards, badges, tooltips, toasts, icons). Use thewix-design-systemskill; only if it is missing, read the installed package's types and name the file.Custom React only after both came back empty, composed from WDS layout primitives. No third UI dependency.
Never skip a step, and never decide a component is missing from memory: check. Skipping to step 2 because a lookup was inconvenient is the most common way a dashboard page ends up all-WDS.
Overlaps: in a patterns page, patterns' header and container win over WDS's. PickerModal, usePickerModal and bulkActionModal are patterns overlays, not WDS modals. Dashboard Plugins render outside a patterns shell, so WDS is the default there; patterns collection components still apply if the surface displays a data collection.
Entity create and edit
Opening a listed record, and any create or edit form for one (even a single field), is an editable EntityPage, not a Dashboard Modal or SidePanel; "I built the list without @wix/patterns" is no exception. A page that lists nothing (settings, config) has no EntityPage obligation. A Dashboard Modal is only for dialogs that neither write nor display a listed record. On the create route, omit entity and read the Create route section of <pkgRoot>/dist/docs/useEntityPage.md. Full flow: COMPONENT_SELECTION.md.
Extension Types Reference Table
| Extension Type | Category | extensionType (for wix generate --params) | Reference File |
|---|---|---|---|
| Dashboard Page | Dashboard | DASHBOARD_PAGE | DASHBOARD_PAGE.md |
| Dashboard Modal | Dashboard | DASHBOARD_MODAL | DASHBOARD_MODAL.md |
| Dashboard Plugin | Dashboard | DASHBOARD_PLUGIN | DASHBOARD_PLUGIN.md |
| Dashboard Menu Plugin | Dashboard | DASHBOARD_MENU_PLUGIN | DASHBOARD_MENU_PLUGIN.md |
| Service Plugin | Backend | SERVICE_PLUGIN | SERVICE_PLUGIN.md |
| App Tools (AI assistant tools) | Backend | APP_TOOLS, then SERVICE_PLUGIN with pluginType: TOOLS_PROVIDER_CONFIG | APP_TOOLS.md |
| Backend Event Extension | Backend | EVENT | BACKEND_EVENT.md |
| Backend API (HTTP endpoint) | Backend | HTTP_ENDPOINT | BACKEND_API.md |
| Data Collection | Backend | DATA_COLLECTION | DATA_COLLECTION.md |
| Editor React component | Site | EDITOR_REACT_COMPONENT | EDITOR_REACT_COMPONENT.md |
| Custom element widget | Site | CUSTOM_ELEMENT | CUSTOM_ELEMENT_WIDGET.md |
| Site Plugin | Site | SITE_PLUGIN | SITE_PLUGIN.md |
| Embedded Script | Site | EMBEDDED_SCRIPT | EMBEDDED_SCRIPT.md |
| Context Provider | Site | CONTEXT_PROVIDER | CONTEXT_PROVIDER.md |
| Function Library | Site | FUNCTION_LIBRARY | FUNCTION_LIBRARY.md |
| Page onLoad (a context provider with no API) | Site | CONTEXT_PROVIDER, named Page On Load <Task Name> | PAGE_ON_LOAD.md |
| Lightbox (popup) open, close and data | Site | rules added to the extension that renders on the site | LIGHTBOX.md |
Key constraints:
Context Provider, Function Library, Page onLoad and Lightbox are for the Studio 2 editor only; they do not run on Harmony, classic Wix Editor or Wix Studio sites.
Dashboard Page cannot use
<Modal />; use a separate Dashboard Modal anddashboard.openModal().A prompt that attaches logic to an element it names as already on the page ("hide Heading 1 on click", "show the footer text only after…") is a Context Provider consumed by Editor Binding, not a new Editor React component.
A page onLoad context covers only the pages it is bound to; "whichever page", "any page" or "every page" means an Embedded Script instead.
A popup that only opens automatically on page load, on chosen pages after a fixed delay, is the popup's auto-open setting in the editor, not code.
HTTP endpoints: use
extensionType: "HTTP_ENDPOINT"(notBACKEND_API); directories, handler types and frontend URLs are in BACKEND_API.md.
Cross-Cutting References
| Topic | Reference |
|---|---|
| Code Quality Requirements (applies to all generated code) | CODE_QUALITY.md |
| Extension Registration | EXTENSION_REGISTRATION.md |
Extension metadata — the name and aiContext to add to each scaffolded builder | EXTENSION_METADATA.md |
| App Validation | APP_VALIDATION.md |
| App Market Review | APP_MARKET_REVIEW.md |
| App Identifiers (Namespace, Code ID) | APP_IDENTIFIERS.md |
| Wix Stores Versioning (V1/V3), including product media writes | STORES_VERSIONING.md |
| Official Documentation Links | DOCUMENTATION.md |
| Wix Patterns Dashboard Pages | WIX_PATTERNS_DOCS.md |
| Business Manager theme — the wrapper every dashboard page, modal and plugin needs | BUSINESS_MANAGER_THEME.md |
Business Manager tokens — --wds-* replacements, the rebased spacing unit, per-component adjustments | BUSINESS_MANAGER_TOKENS.md |
Business Manager theme for @wix/patterns — the second wrapper that turns on the patterns shell's Harmony layout | BUSINESS_MANAGER_PATTERNS.md |
| Dashboard UX Success Model (what a good dashboard contains) | UX_SUCCESS_MODEL.md |
| Draft template — start here for any dashboard page: finding, copying and adapting the package's page templates | DRAFT_TEMPLATE.md |
The state object useTableCollection() returns | TABLE_STATE.md |
| Finding the SDK method and field names behind a page | DATA_SOURCES.md |
| Display images from CMS, vertical SDKs, and custom collections | IMAGE_RENDERING.md |
| Edit entity images with Media Manager; draft, Save/Cancel, and picker verification | IMAGE_EDITING.md |
| Filter paths, WQL operators and cursor paging | QUERY_AND_PAGING.md |
SDK-First Rule (Existing Wix App Data Is Never CMS)
CRITICAL: Data owned by an existing Wix business app is read and written through that app's SDK module — NEVER modeled as a new CMS Data Collection. A custom collection for such data starts empty and stays disconnected from the real records (e.g., a "refunds dashboard" built on CMS shows an empty state while refunded orders exist in Wix eCommerce).
Find the entity the user mentioned in the entity → SDK module map and use that package; the SDK lookup then names the namespace and method. If the entity isn't listed and the lookup finds nothing, run SearchWixSDKDocumentation for it — never conclude CMS without one of the two. CMS is only for data your app itself introduces (configuration, rules, app-specific records) that no Wix app manages.
SDK types: access them through the namespace you import, as <namespace>.<TypeName>, using any type name shown in the docs — never import a type by name from the @wix/<pkg> root.
import { orders } from '@wix/ecom';
const rows: orders.Order[] = []; // ✅
// import type { Order } from '@wix/ecom'; // ❌ has no exported member 'Order'A map keyed by an SDK enum: fields are typed <Enum>WithLiterals (the enum or its string values). Partial<Record<bookings.BookingStatusWithLiterals, V>> cannot be indexed by a status (TS7053). Use Record<…WithLiterals, V> with an entry for every value, or key a partial map by the string values: Partial<Record<`${bookings.BookingStatus}`, V>>.
Data Collection Inference
CRITICAL: Data collections are often needed implicitly — don't wait for the user to explicitly say "create a CMS collection." Infer the need automatically.
⚠️ Apply the SDK-First Rule first — the indicators below only apply to data your app itself owns, not to entities a Wix app already manages.
Skip this section if the user provides a collection ID directly (e.g., an existing site-level collection). In that case, use the provided ID as-is — no Data Collection extension or namespace scoping needed.
Always include a Data Collection extension when ANY of these are true:
| Indicator | Example |
|---|---|
| User mentions saving/storing/persisting app-specific data | "save the fee amount", "store product recommendations" |
| A dashboard page will manage (CRUD) domain entities | "dashboard to manage fees", "admin page to edit rules" |
| A service plugin reads app-configured data at runtime | "fetch fee rules at checkout", "look up shipping rates" |
| User mentions "dedicated database/collection" | "save in a dedicated database collection" |
| Multiple extensions reference the same custom data | Dashboard manages fees + service plugin reads fees |
Why this matters: Without the Data Collection extension, the collection won't be created when the app is installed, the Wix Data APIs may not work (code editor not enabled), and collection IDs won't be properly scoped to the app namespace.
If data collection is inferred, follow the App Namespace Requirement to obtain the namespace before proceeding.
App Namespace Requirement
When creating a Data Collection, you MUST ask the user for their app namespace from Wix Dev Center. This is a required parameter that must be obtained from the user's Dev Center dashboard and cannot be recommended or guessed.
If the user hasn't provided their app namespace, read APP_IDENTIFIERS.md and give the user the instructions to obtain it.
Collection ID Coordination
Applies ONLY when a Data Collection extension is being created. If the user provides a collection ID directly, use it as-is — no namespace scoping, no Data Collection extension needed.
When a Data Collection is created alongside other extensions that reference the same collections:
Get the app namespace (see App Namespace Requirement above)
Determine the
idSuffixfor each collection (the Data Collection reference documents the full ID format)Use the full scoped collection ID (
<app-namespace>/<idSuffix>) in all extensions that reference the collection via Wix Data API calls
Wix Stores Versioning Requirement
Applies when ANY Wix Stores API is used (products, inventory, orders, etc.):
Read the Stores Versioning reference — see STORES_VERSIONING.md. It contains the module map, permissions cheatsheet, copy-paste dual-catalog recipes (list/get/create/update/delete products, inventory, categories), the V1→V3 field map, webhook mapping, and the major V3 gotchas. Use it before searching SDK docs — it covers the common 80%.
All Stores operations must check catalog version first using
getCatalogVersion()Use the correct module based on version:
productsV3(V3) vsproducts(V1)Apps MUST support both V1 and V3 — single-version apps cannot list in the App Market and break on new sites
Request both V1 and V3 permission scopes for every Stores operation
This is non-negotiable — V1 and V3 are NOT backwards compatible.
Identity and Elevation Requirement
Applies whenever an extension calls a Wix SDK method. Decide where the call runs before writing it.
Who the extension runs as decides everything below — the Category column in Extension Types Reference Table tells you which one you have:
Site and editor extensions — custom element widgets, site plugins, Editor React components, embedded scripts — run as the site visitor or member, never as the app.
Dashboard extensions run as the Wix user — not as a site visitor, and not as the app.
Backend extensions — Backend API, Backend Event, Service Plugin — run as the app.
auth.elevate works only in backend code. In a site, editor, or dashboard extension it doesn't work at all.
Default: call the SDK directly from the extension. Routing a call that didn't need it is not a harmless extra hop — it is how working features break. Sort by who the call acts for, never by its scope name:
Acts for the current visitor or member — their cart, checkout, booking, order, reservation, or profile:
currentCartV2.*,cartV2.placeOrder,bookings.createBooking,members.getCurrentMember(REST Get My Member), and anything else operating on "my" or "the current" entity. These resolve the actor from the caller's session, so elevating runs them as the app and detaches the result from the person who asked — an order with no buyer, a booking with no attendee.The platform filters the result by caller — an elevated call returns what the direct call withheld, so a "fix" for a sparse result becomes a leak. Wix Data
items.*follows the collection'sdataPermissions(scaffolded default:itemRead: 'ANYONE', writes'PRIVILEGED'— fix writes with permissions, not routing; see DATA_COLLECTION.md). Catalog reads return base fields to anyone, withholdingMERCHANT_DATAand non-visible products unless the app holdsSCOPE.STORES.PRODUCT_READ_ADMIN.members.getMember/queryMemberswithholdPRIVATEmembers from visitor and member callers.
Route out only when the method acts on the business as a whole, which a visitor or member genuinely cannot do: archiveLocation, queryLocations, catalog and inventory writes, bookings.confirmBooking, order management. When the method's docs show the call elevated, route it out and elevate there — from any host, dashboard included, because auth.elevate only works in backend code, so the endpoint is the only place the documented pattern can run.
When you're unsure in a site, editor, or dashboard extension, call it directly and let it fail. A method a site extension may not call returns a permission error you see immediately; a method wrongly routed and elevated succeeds and silently returns the wrong data or acts for the wrong person. Some method pages carry a prose note that settles it — get-my-member: "This method requires visitor or member authentication." Authoritative when present (the SDK lookup prints it as the method's note), but only a minority of pages have one, so its absence decides nothing.
Two signals never settle it: the scope name — the methods whose scope name points the wrong way are rules in sdk-curated.json, which the SDK lookup prints as note — and the SDK schema line's client prefix, which varies by docs channel for the same method, so carries nothing and isn't worth re-deriving.
Routing out means a Backend API endpoint that elevates and is reached with httpClient.fetchWithAuth(). Elevation bypasses Wix's permission check, so the endpoint must re-check the caller itself — see Identity and Authorization for what each host can actually verify, and why an owner-only operation belongs in a dashboard extension instead.
Some extensions or SDK calls require a permission scope that wix generate doesn't add automatically. Adding one is a Dev Center account change, not something the agent does — tell the user which scope to add: open their app at https://manage.wix.com/apps/<appId>/home, select Develop > Permissions in the left menu, then Add Permissions, then report it under Manual Steps Required. If the app is already installed on a site, the owner must also re-approve it via the install/update flow — revisit that same app page's "Test App" flow (or the release output's install links) and accept "Agree & Update" — before the scope takes effect there.
App Market Review
Applies when a user wants to submit their app to the Wix App Market, list it publicly, prepare for App Market review, audit decline risk, or fix App Market review feedback. Not needed for private apps or routine version releases.
Read APP_MARKET_REVIEW.md — it contains the full technical checklist, implementation notes with Wix doc links, and the review taxonomy IDs for traceability.
Implementation Workflow
Step 1: Ask Clarifying Questions (if needed)
Only ask for configuration values when absolutely necessary for the implementation to proceed. If a value can be configured later or added as a manual step, don't block on it.
If unclear on approach (placement, visibility, configuration, integration), ask clarifying questions. If the answer could change the extension type, wait for the response before proceeding. Otherwise, proceed with the best-fit extension type.
Step 2: Make Your Recommendation
Use the Extension Types Reference Table and decision content above. State extension type and brief reasoning (placement, functionality, integration).
Step 3: Read Extension Reference, Check API References, Then Discover (if needed)
Workflow: Read extension reference → Check API references → SDK lookup for vertical entities → docs only for what the lookup does not print.
Read the extension reference file for the chosen extension type from the table above
Identify required APIs from user requirements
Check relevant API reference files:
Backend events →
references/backend-event/COMMON-EVENTS.mdWix Data →
references/data-collection/WIX_DATA.mdDashboard SDK →
references/dashboard-page/DASHBOARD_API.md; Media Manager →references/dashboard-page/DASHBOARD_API_MEDIA_AND_PLUGINS.mdService Plugin SPIs → read
references/SERVICE_PLUGIN.md, then the SPI file its References table linksApp Tools (AI assistant tools) → read
references/APP_TOOLS.md; it links toreferences/app-tools/TOOLS.md(declaration) andreferences/service-plugin/TOOLS_PROVIDER.md(handler)
Verify the specific method/event exists in references
For vertical entities, run the SDK lookup once, with every entity; use MCP discovery only for what neither the references nor the lookup answer
Platform APIs (never discover - in references):
Wix Data, Dashboard SDK, Event SDK (common events), Service Plugin SPIs
Vertical APIs (discover if needed):
Wix Stores (⚠️ MUST use Stores Versioning reference — V1/V3 catalog check required), Wix eCommerce, Wix Bookings, Wix Members, Wix Pricing Plans, third-party integrations — find the right
@wix/*package in the SDK-First Rule module map first, install it, then get the namespace and method from the SDK lookup
Decision table:
| User Requirement | Check References / Discovery Needed? | Reason / Reference File |
|---|---|---|
| "Display store products" | ✅ YES (SDK lookup) | Wix Stores API — include Stores Versioning reference |
| "Dashboard for orders / refunds" | ✅ YES (SDK lookup) | Wix eCommerce API (@wix/ecom) — NEVER a CMS collection |
| "Show booking calendar" | ✅ YES (SDK lookup) | Wix Bookings API (@wix/bookings); SPI leaves cover only service plugins |
| "Send emails to users" | ✅ YES (SDK lookup, then MCP) | Wix Triggered Emails not in reference files |
| "Get member info" | ✅ YES (SDK lookup) | Wix Members API not in reference files |
| "Listen for cart events" | Check COMMON-EVENTS.md | MCP discovery only if event missing in reference |
| "Store data in collection" | WIX_DATA.md ✅ Found | ❌ Skip discovery (covered by reference) |
| "Create CMS collections for my app" | Data Collection reference | ❌ Skip discovery (covered by dedicated reference) |
| "Show dashboard toast" | DASHBOARD_API.md ✅ Found | ❌ Skip discovery |
| "Show toast / navigate" | DASHBOARD_API.md ✅ Found | ❌ Skip discovery |
| "UI only (forms, inputs)" | N/A (no external API) | ❌ Skip discovery |
| "Settings page with form inputs" | N/A (UI only, no external API) | ❌ Skip discovery |
| "Dashboard page with local state" | N/A (no external API) | ❌ Skip discovery |
SDK lookup: one call for every vertical entity
node <this-skill-dir>/scripts/sdk-lookup.cjs contacts orders --intent search # search | read | write | event | allRun it from the app directory, after the vertical packages are installed, with every entity the feature touches in one call. Pick the intent from the feature, once. A dashboard collection page edits its records by default (DRAFT_TEMPLATE.md), so it uses all: that holds the search and write lines, the search methods first. Use search (with a search box) or read only for a read-only page, and write for a feature that only writes. Do not pipe the output through head. Per entity it prints:
the namespace to use and why (a curated rule; within one package, a
search*method for a search, then the highest API version; across packages that export the same name, the module map's package, else the package named after it), and the other namespaces — across packages, each with its entity description, so switch when the feature matches another one;the import line;
shape: the one call to write for the intent, with its reason and, for a search, where the term goes;each method for the intent, with its call shape (
search,query-builder,query, …), permission, required fields (revision) and its docs notes (note, with an update's field rules: what a missing field does, and how to clear one);limits, the size and range caps its request declares (orderIds 1–100 items,limit() ≤1000): keep a pagelimitwithin the cap of a batch method it feeds;fieldsetswhen an option decides which fields come back (members default toPUBLIC, withoutloginEmailorcontact: ask for the set that holds every field you show); for a search, query or list method alsoargandreturns, the keys of its argument (cursorPaging,filter,sort,search) and of its response (pagingMetadata.cursors.next); a filter list only when the package declares one (otherwise the method docs page lists the fields) — not who may call it: that stays with Identity and Elevation Requirement;the entity type as
file:line— the declaration to verify fields against (DATA_SOURCES.md);each method's own declaration as
decl file:line— read the types behindargandreturnsthere, not by searching the typings;the curated rules for the entity (
note), such as the Stores V1/V3 check.
Take the namespace, the method and the call shape from its output; do not re-derive them from the typings, and follow the lines under shape. When the lookup prints site, the choice depends on the site (Stores: call getCatalogVersion() and support both). not installed prints the install command: install, then run the lookup again. A name it does not know exits 1 with near matches.
It prints each search and query method's closed filter list (free-text fields, operators per field, sort) from the package's own declarations. It prints a method's permission scope ID only when the package declares it; when the output says scope: not in the package, get the scope from the method's docs page (the next list).
MCP Tools for discovery (for what the lookup does not print):
SearchWixSDKDocumentation- SDK methods and APIs (Always use maxResults: 5)ReadFullDocsMethodSchema- Full type schema for a specific SDK method (parameters, return type, permissions)ReadFullDocsArticle- Prose guides and conceptual articles only (not for SDK method signatures)
Step 4a: Scaffold via the CLI
For each supported type, including HTTP endpoints, run npx wix generate --params '<json>'. The command returns {"success":true,"extensionType":"...","newFiles":[...]} on success.
If the command fails because of unknown or invalid params, run npx wix schema generate --type <extensionType> to print the JSON Schema for that extension type, fix the --params payload, and retry. Do not fall back to manual scaffolding. The one exception is HTTP_ENDPOINT on a CLI older than 1.1.243, which predates the generator but still supports the extension: create the endpoint file by hand as described in BACKEND_API.md.
What the CLI does automatically:
Creates folders and stub files
For registered extensions, generates a fresh UUID and updates
src/extensions.tswith the import and.use()callFor HTTP endpoints, creates the route file without changing
src/extensions.tsEnforces naming rules (kebab-case, hyphen-required custom elements, etc.)
HTTP endpoints: Run npx wix generate --params '{"extensionType":"HTTP_ENDPOINT","name":"hello"}', then implement the handler in the returned file. Follow BACKEND_API.md; if the route does not respond, its troubleshooting hint shows how to confirm discovery from the build output.
Step 4b: Fill in business logic
Open every path returned in newFiles and replace stubbed handler bodies / UI / queries with the user's actual logic, guided by the extension reference file's API and configuration sections.
⚠️ MANDATORY when using WDS: Invoke the
wix-design-systemskill before editing your first.tsx/.jsxfile that imports@wix/design-system. Do NOT invoke it preemptively for backend-only or data-only jobs — it adds large content to context that you won't use.Events, service plugins, data collections, and dashboard pages, modals, dashboard plugins and dashboard menu plugins: finish each
*.extension.tswith thenameandaiContextits builder declares, per EXTENSION_METADATA.md.⚠️ MANDATORY when using Data Collections: Use the EXACT collection ID from
idSuffix(case-sensitive). IfidSuffixis"product-recommendations", use<app-namespace>/product-recommendationsNOTproductRecommendations.
Step 4c: UX Completeness Self-Audit
Dashboard page UI only. tsc, wix build, and wix preview all check that the code compiles and runs — none of them check that it's the dashboard the UX Success Model describes. A page with a bare, un-summarized, un-openable table compiles cleanly and still fails the requirement — that gap is exactly how a generated dashboard passes every technical check and still disappoints. Measured runs confirm it: a page can compile clean and still ship with none of the three items below, because the earlier checklist entries were a stated intention rather than something re-checked against the code that actually landed.
Before moving to Step 5, re-open every page file you just wrote and check the actual code — not what you intended to include:
[ ] [ ] The page has a
SummaryBaronly if the request asked for one. Grep for it: an uninvited bar is a defect, not a bonus, and deleting it is the fix. If the request did ask, every metric earns its place and the headline counts what matches the filters (state.collection.total, fed byfetchTotal), not what has been paged in — any metric derived fromkeyedItemsis labelled as such.[ ] [ ] If a
SummaryBarnumber is fed byfetchTotal, follow that function to the call it makes and confirm the call counts. AfetchTotalthat resolvesundefined— the usual cause beingpagingMetadata.total, which a cursor-paged response does not carry — makes the bar report0beside a table full of rows, and it compiles, runs and passes every other check on this list. It must resolve a number from a count endpoint (items.query(id)…count(), a vertical's own count, or offset paging withreturnTotalCount: true); if the API has none, deletefetchTotaland label the metric as loaded rows. See TABLE_STATE.md.[ ] [ ] Every row opens an editable entity page by default:
onRowClickcallsnavigateToEntityPage, and the destination has anEntityPage, a working form, and a real save operation. A read-only route is justified by one of the three cases in DRAFT_TEMPLATE.md, and the final response names which one; report-only/export-only requests need no drill-in. The save calls the source's real update method and lets a failed call throw, so the page shows its error — no no-op, local-state-only save, orcatchthat swallows the failure. If Preview is available, change one field, reload to confirm it saved, and change it back; otherwise report the save as unverified. ASidePanelin a collection page file is the defect this replaces.grep -n "<SidePanel" <page files>should return nothing — match the JSX tag, not the bare word, or the templates' own "never a SidePanel" comments fail the check and invite someone to "fix" correct code.[ ] [ ] Every filter name declared in the toolbar also appears inside
fetchData's query construction — grep for the name in both places if unsure.[ ] [ ] The table wires
errorState— without it a failed query is indistinguishable from a slow one, and the page you just shipped cannot tell you which it is.[ ] [ ] Routed templates only (all but the settings one) — the entry file both passes and guards
location.PatternsReactRouterthrows at open whenlocationis missing or stillundefinedon the first render, andtsc,wix buildand even a green build all pass regardless. Both halves are required — thelocation={location}prop and thelocation ? … : nullguard around it, sinceobserveStatehas not fired yet on the first render. Grep the entry file rather than trusting recall:grep -n "observeState\|location={location}\|location ?" src/extensions/dashboard/pages/<page>/<page>.tsx # the file the builder's `component` points atThree hits is correct. A missing guard is the failure mode that has actually shipped: a measured run produced a page whose plumbing looked present and still crashed on open, while a re-run of the same prompt produced a working one — so this is intermittent, and re-running is not a check.
[ ] [ ] Every
@wix/*vertical imported by the page's api module is a declared dependency, and each one's scope is listed under Manual Steps. Any call to a secondary vertical (an enrichment lookup, a filter's options, a search term resolved to ids) is wrapped so its failure degrades that feature instead of failing the page.
If a box fails and no exception applies, add the missing piece now. Do not let "it compiles" stand in for "it satisfies the checklist" — Step 5 checks the former, this step checks the latter, and they are independent.
Step 5: Run Validation
Run the four steps in Validation below. Do NOT report completion to the user until validation passes — if it fails, fix the errors and re-validate until it does.
Step 6: Report Completion
Only after validation passes, provide a concise summary section at the top of your response:
## ✅ Implementation Complete
[1-2 sentence description of what was built]
**Extensions Created:**
- [Extension 1 Name] - [Brief purpose]
- [Extension 2 Name] - [Brief purpose]
**Build Status:**
- ✅ Dependencies: [Installed / status message]
- ✅ TypeScript: [No compilation errors / status]
- ✅ Build: [Completed successfully / status]
- ✅/⚠️ Preview: [Created — Dashboard URL / Failed - reason]
**⚠️ IMPORTANT: [X] manual step(s) required to complete setup** (see "Manual Steps Required" section below)If there are NO manual steps, state: "✅ No manual steps required — you're ready to go!"
Step 7: Surface Manual Action Items
Present any manual steps the user must perform (e.g., configuring settings in the Wix dashboard, enabling permissions, setting up external services).
Format:
## 🔧 Manual Steps Required
The following actions need to be done manually by you:
### 1. [Action Category/Title]
[Detailed description with specific instructions]
### 2. [Action Category/Title]
[Detailed description]Extension Registration
wix generate --params updates src/extensions.ts automatically for registered extensions. HTTP endpoints require no import or .use() call; the runtime discovers their files. For background, troubleshooting, and the manual recovery pattern when src/extensions.ts drifts, see EXTENSION_REGISTRATION.md.
Validation
Execute these steps sequentially after all implementation is complete. See APP_VALIDATION.md for the complete guide. Dashboard page UI: run Step 4c's UX Completeness Self-Audit first — the checks below verify the code runs, not that it's the dashboard the prompt asked for.
Package Installation — Detect package manager, run install
TypeScript Compilation —
npx tsc --noEmit -p .Build —
npx wix buildPreview —
npx wix preview, in the foreground and unpiped: it uploads, prints the preview URLs and exits on its own, so notimeout, backgrounding,sleepor| tail. If your shell moves it to the background anyway, it stalled: read what it printed and.wix/debug.logonce, then report that the preview did not complete. Don't poll, and don't promise a URL
Stop and report errors if any step fails. Check .wix/debug.log on failures.
Documentation
For links to official Wix CLI documentation for all extension types, see DOCUMENTATION.md.
Files
- ATTRIBUTION.mdAdded by AutoGineer801 B
- LICENSELicence file added by AutoGineer1.0 KiB
- SKILL.md51.5 KiB
- references/APP_IDENTIFIERS.md1.6 KiB
- references/APP_MARKET_REVIEW.md9.7 KiB
- references/APP_TOOLS.md5.8 KiB
- references/APP_VALIDATION.md6.3 KiB
- references/BACKEND_API.md9.5 KiB
- references/BACKEND_EVENT.md4.0 KiB
- references/BUSINESS_MANAGER_PATTERNS.md3.7 KiB
- references/BUSINESS_MANAGER_THEME.md7.9 KiB
- references/BUSINESS_MANAGER_TOKENS.md6.7 KiB
- references/CODE_QUALITY.md2.2 KiB
- references/CONTEXT_PROVIDER.md9.8 KiB
- references/CUSTOM_ELEMENT_WIDGET.md9.7 KiB
- references/DASHBOARD_MENU_PLUGIN.md3.9 KiB
- references/DASHBOARD_MODAL.md9.8 KiB
- references/DASHBOARD_PAGE.md9.7 KiB
- references/DASHBOARD_PLUGIN.md5.7 KiB
- references/DATA_COLLECTION.md8.7 KiB
- references/DOCUMENTATION.md5.1 KiB
- references/EDITOR_REACT_COMPONENT.md9.7 KiB
- references/EMBEDDED_SCRIPT.md11.4 KiB
- references/EXTENSION_METADATA.md3.6 KiB
- references/EXTENSION_REGISTRATION.md2.7 KiB
- references/FUNCTION_LIBRARY.md6.6 KiB
- references/LIGHTBOX.md9.7 KiB
- references/PAGE_ON_LOAD.md8.1 KiB
- references/SDK_MODULE_MAP.md2.8 KiB
- references/SERVICE_PLUGIN.md7.7 KiB
- references/SITE_PLUGIN.md9.6 KiB
- references/STORES_VERSIONING.md9.7 KiB
- references/WIX_PATTERNS_DOCS.md4.9 KiB
- references/app-tools/TOOLS.md4.2 KiB
- references/backend-event/COMMON-EVENTS.md4.9 KiB
- references/context-provider/CONSUMING.md6.0 KiB
- references/context-provider/CONTEXT_PROVIDER_SPEC.md8.1 KiB
- references/context-provider/CONTEXT_SPEC_FUNCTIONS.md4.5 KiB
- references/context-provider/CONTEXT_SPEC_ITEMS.md7.4 KiB
- references/context-provider/DERIVED_VALUES.md5.5 KiB
- references/context-provider/EXAMPLES.md5.2 KiB
- references/context-provider/LOADING.md3.8 KiB
- references/context-provider/LOADING_DESIGN.md7.3 KiB
- references/context-provider/LOADING_PATTERN.md8.5 KiB
- references/context-provider/LOADING_REQUIREMENTS.md7.2 KiB
- references/context-provider/LOADING_RULES.md5.9 KiB
- references/context-provider/MANIFEST.md7.9 KiB
- references/context-provider/PROVIDER_PATTERN.md4.4 KiB
- references/context-provider/VERTICAL_CONTEXTS.md9.6 KiB
- references/context-provider/vertical-contexts/blog/blog-feed-2.md1.8 KiB
- references/context-provider/vertical-contexts/blog/blog-feed.md8.7 KiB
- references/context-provider/vertical-contexts/blog/category-item.md2.8 KiB
- references/context-provider/vertical-contexts/blog/post-by-id-2.md2.1 KiB
- references/context-provider/vertical-contexts/blog/post-by-id.md8.9 KiB
- references/context-provider/vertical-contexts/blog/post-list-item.md6.6 KiB
- references/context-provider/vertical-contexts/blog/post.md8.6 KiB
- references/context-provider/vertical-contexts/blog/related-posts.md3.1 KiB
- references/context-provider/vertical-contexts/blog/tag-item.md1.8 KiB
- references/context-provider/vertical-contexts/bookings/booking-checkout.md5.3 KiB
- references/context-provider/vertical-contexts/bookings/categories.md2.2 KiB
- references/context-provider/vertical-contexts/bookings/locations.md2.4 KiB
- references/context-provider/vertical-contexts/bookings/service-2.md8.9 KiB
- references/context-provider/vertical-contexts/bookings/service-3.md6.3 KiB
- references/context-provider/vertical-contexts/bookings/service-4.md9.1 KiB
- references/context-provider/vertical-contexts/bookings/service-5.md692 B
- references/context-provider/vertical-contexts/bookings/service-addons.md2.0 KiB
- references/context-provider/vertical-contexts/bookings/service.md7.4 KiB
- references/context-provider/vertical-contexts/bookings/services.md2.6 KiB
- references/context-provider/vertical-contexts/bookings/time-slots-2.md9.1 KiB
- references/context-provider/vertical-contexts/bookings/time-slots-3.md1.3 KiB
- references/context-provider/vertical-contexts/bookings/time-slots.md8.8 KiB
- references/context-provider/vertical-contexts/events/event-2.md9.0 KiB
- references/context-provider/vertical-contexts/events/event-3.md1.4 KiB
- references/context-provider/vertical-contexts/events/event-by-id-2.md9.2 KiB
- references/context-provider/vertical-contexts/events/event-by-id-3.md1.6 KiB
- references/context-provider/vertical-contexts/events/event-by-id-4.md9.1 KiB
- references/context-provider/vertical-contexts/events/event-by-id-5.md8.6 KiB
- references/context-provider/vertical-contexts/events/event-by-id-6.md3.3 KiB
- references/context-provider/vertical-contexts/events/event-by-id.md979 B
- references/context-provider/vertical-contexts/events/event-by-slug-2.md9.1 KiB
- references/context-provider/vertical-contexts/events/event-by-slug-3.md1.5 KiB
- references/context-provider/vertical-contexts/events/event-by-slug.md887 B
- references/context-provider/vertical-contexts/events/event-item-2.md9.1 KiB
- references/context-provider/vertical-contexts/events/event-item-3.md1.5 KiB
- references/context-provider/vertical-contexts/events/event-item.md856 B
- references/context-provider/vertical-contexts/events/event-list.md5.7 KiB
- references/context-provider/vertical-contexts/events/event.md1.1 KiB
- references/context-provider/vertical-contexts/events/registration.md6.6 KiB
- references/context-provider/vertical-contexts/events/seating.md5.4 KiB
- references/context-provider/vertical-contexts/events/thank-you-2.md9.1 KiB
- references/context-provider/vertical-contexts/events/thank-you-3.md9.1 KiB
- references/context-provider/vertical-contexts/events/thank-you-4.md3.3 KiB
- references/context-provider/vertical-contexts/events/thank-you.md1.2 KiB
- references/context-provider/vertical-contexts/stores/category-by-id.md1.5 KiB
- references/context-provider/vertical-contexts/stores/category-by-slug.md1.6 KiB
- references/context-provider/vertical-contexts/stores/category-info.md3.9 KiB
- references/context-provider/vertical-contexts/stores/category-list.md1.2 KiB
- references/context-provider/vertical-contexts/stores/product-2.md9.0 KiB
- references/context-provider/vertical-contexts/stores/product-by-id-2.md9.2 KiB
- references/context-provider/vertical-contexts/stores/product-by-id-3.md9.2 KiB
- references/context-provider/vertical-contexts/stores/product-by-id-4.md4.3 KiB
- references/context-provider/vertical-contexts/stores/product-by-id-5.md9.2 KiB
- references/context-provider/vertical-contexts/stores/product-by-id-6.md9.2 KiB
- references/context-provider/vertical-contexts/stores/product-by-id.md1.2 KiB
- references/context-provider/vertical-contexts/stores/product-by-slug-2.md9.0 KiB
- references/context-provider/vertical-contexts/stores/product-by-slug.md908 B
- references/context-provider/vertical-contexts/stores/product-item-2.md9.0 KiB
- references/context-provider/vertical-contexts/stores/product-item-3.md3.2 KiB
- references/context-provider/vertical-contexts/stores/product-item.md1.3 KiB
- references/context-provider/vertical-contexts/stores/product-list-2.md9.1 KiB
- references/context-provider/vertical-contexts/stores/product-list-3.md630 B
- references/context-provider/vertical-contexts/stores/product-list.md4.4 KiB
- references/context-provider/vertical-contexts/stores/product.md1.2 KiB
- references/context-provider/vertical-contexts/stores/promotion-item.md1.7 KiB
- references/custom-element-widget/SETTINGS_PANEL.md2.5 KiB
- references/dashboard-menu-plugin/blog-slots.md2.6 KiB
- references/dashboard-menu-plugin/bookings-slots.md1.6 KiB
- references/dashboard-menu-plugin/crm-slots.md277 B
- references/dashboard-menu-plugin/ecommerce-slots.md858 B
- references/dashboard-menu-plugin/events-slots.md940 B
- references/dashboard-menu-plugin/restaurants-slots.md497 B
- references/dashboard-menu-plugin/stores-slots.md1.4 KiB
- references/dashboard-page/API_SPEC.md3.1 KiB
- references/dashboard-page/COMPONENT_SELECTION.md8.8 KiB
- references/dashboard-page/DASHBOARD_API.md8.3 KiB
- references/dashboard-page/DASHBOARD_API_MEDIA_AND_PLUGINS.md6.5 KiB
- references/dashboard-page/DASHBOARD_API_PAGE_HELPERS.md5.4 KiB
- references/dashboard-page/DASHBOARD_API_PAGE_IDS.md5.5 KiB
- references/dashboard-page/DATA_SOURCES.md9.1 KiB
- references/dashboard-page/DRAFT_TEMPLATE.md9.8 KiB
- references/dashboard-page/DYNAMIC_PARAMETERS.md9.6 KiB
- references/dashboard-page/ECOM_NAVIGATION.md4.1 KiB
- references/dashboard-page/IMAGE_EDITING.md2.3 KiB
- references/dashboard-page/IMAGE_RENDERING.md3.5 KiB
- references/dashboard-page/QUERY_AND_PAGING.md9.0 KiB
- references/dashboard-page/SDK_PACKAGE_LAYOUT.md1.5 KiB
- references/dashboard-page/TABLE_STATE.md7.1 KiB
- references/dashboard-page/UX_SUCCESS_MODEL.md9.8 KiB
- references/dashboard-page/WDS_LAYOUT.md9.6 KiB
- references/dashboard-plugin/SLOTS.md9.4 KiB
- references/data-collection/LIFECYCLE.md3.9 KiB
- references/data-collection/PERMISSIONS.md3.3 KiB
- references/data-collection/SCHEMA.md8.1 KiB
- references/data-collection/WIX_DATA.md9.8 KiB
- references/editor-react-component/ACCESSIBILITY.md9.4 KiB
- references/editor-react-component/ANIMATED-COMPONENTS.md9.7 KiB
- references/editor-react-component/BRANDED-COMPONENTS.md9.8 KiB
- references/editor-react-component/BROWSER-SUPPORT.md2.7 KiB
- references/editor-react-component/CANVAS-COMPONENTS.md9.8 KiB
- references/editor-react-component/COMPONENT-CONTRACT.md9.5 KiB
- references/editor-react-component/COMPONENT-PREVIEW.md2.8 KiB
- references/editor-react-component/CSS-GUIDELINES.md9.8 KiB
- references/editor-react-component/DEFAULT-IMAGES.md4.1 KiB
- references/editor-react-component/DESIGN-STATES.md9.8 KiB
- references/editor-react-component/DIRECTIONALITY.md2.5 KiB
- references/editor-react-component/DISABLED-INVALID.md1.5 KiB
- references/editor-react-component/EDITOR-EXTENSION-CONFIGURATION.md5.0 KiB
- references/editor-react-component/FUNCTION-HANDLERS.md9.0 KiB
- references/editor-react-component/MANIFEST-ERRORS.md2.7 KiB
- references/editor-react-component/OVERFLOW.md7.5 KiB
- references/editor-react-component/PARTS.md6.5 KiB
- references/editor-react-component/PROPS-VS-CSS.md3.6 KiB
- references/editor-react-component/REACT-GUIDELINES.md4.1 KiB
- references/editor-react-component/SITE-CONTEXT-HOOKS.md7.0 KiB
- references/editor-react-component/SSR.md1.8 KiB
- references/editor-react-component/USER-SOURCES.md9.7 KiB
- references/editor-react-component/VECTOR-ART.md3.0 KiB
- references/editor-react-component/VISUAL-QUALITY.md9.2 KiB
- references/function-library/DATA_TYPES.md3.6 KiB
- references/function-library/EXAMPLES.md8.0 KiB
- references/function-library/FUNCTION_LIBRARY_SPEC.md8.2 KiB
- references/function-library/PARAMETER_PATTERNS.md5.4 KiB
- references/function-library/TROUBLESHOOTING.md2.7 KiB
- references/lightbox/EXAMPLES.md7.1 KiB
- references/lightbox/EXAMPLES_CONDITION.md5.7 KiB
- references/page-on-load/EXAMPLES.md4.3 KiB
- references/page-on-load/PROVIDER.md6.2 KiB
- references/service-plugin/ADDITIONAL-FEES.md3.7 KiB
- references/service-plugin/AVAILABILITY-TIME-SLOTS-CONFIGURATION.md8.2 KiB
- references/service-plugin/BOOKING-POLICY.md3.6 KiB
- references/service-plugin/BOOKINGS-PRICING.md4.9 KiB
- references/service-plugin/BOOKINGS-STAFF-SORTING.md4.4 KiB
- references/service-plugin/BOOKINGS-VALIDATION.md8.1 KiB
- references/service-plugin/DISCOUNT-TRIGGERS.md5.2 KiB
- references/service-plugin/GIFT-CARDS.md3.9 KiB
- references/service-plugin/PAYMENT-SETTINGS.md4.0 KiB
- references/service-plugin/REALTIME-PERMISSIONS.md3.1 KiB
- references/service-plugin/SHIPPING-RATES.md4.5 KiB
- references/service-plugin/TOOLS_PROVIDER.md2.9 KiB
- references/service-plugin/VALIDATIONS.md2.4 KiB
- references/site-plugin/EXAMPLES.md8.7 KiB
- references/site-plugin/SLOTS.md10.2 KiB
- references/stores/CATEGORIES.md1.1 KiB
- references/stores/GET_PRODUCT.md4.2 KiB
- references/stores/INVENTORY.md1.0 KiB
- references/stores/QUERY.md4.4 KiB
- references/stores/WRITE.md5.5 KiB
- scripts/default-animation.json4.7 KiB
- scripts/pkg-root.cjsScript2.1 KiB
- scripts/render-images.cjsScript8.9 KiB
- scripts/scan-a11y-code.cjsScript33.7 KiB
- scripts/scan-a11y-eslint.cjsScript10.9 KiB
- scripts/scan-a11y-render.cjsScript19.7 KiB
- scripts/scan-a11y-review.cjsScript13.5 KiB
- scripts/scan-user-source.cjsScript46.6 KiB
- scripts/sdk-curated.json2.6 KiB
- scripts/sdk-lookup.cjsScript59.5 KiB
- scripts/upload-media.cjsScript14.9 KiB
scripts/scan-user-source.cjs
#!/usr/bin/env node
'use strict';
// oxlint-disable no-console
/**
* Checks a component built from the user's code before `validate`: one command,
* one JSON report.
*
* node <SKILL_ROOT>/scripts/scan-user-source.cjs <component-dir> [--source <file|dir> ...]
*
* Run from the app root. The user's files default to `.wix/agentInput/attachments`
* (unzipped folders included); pass `--source` for code saved anywhere else.
* Each failure names the `USER-SOURCES.md` step it enforces. Warnings are things
* the script can't decide; check each one yourself.
*
* Exit codes: 0 no failures, 1 at least one failure, 2 the script could not run.
*/
const fs = require('fs');
const path = require('path');
const DEFAULT_SOURCE_DIR = '.wix/agentInput/attachments';
const CODE_EXTENSIONS = new Set(['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs']);
const SOURCE_EXTENSIONS = new Set([
...CODE_EXTENSIONS,
'.css',
'.html',
'.vue',
'.svelte',
'.json',
]);
const IMAGE_EXTENSIONS = new Set(['.png', '.jpg', '.jpeg', '.webp', '.gif', '.avif']);
// Project and tool configuration that comes along in a zipped project; never content.
const CONFIG_JSON = /^(?:package(?:-lock)?|tsconfig.*|jsconfig.*|.*\.config|\.?[\w-]*rc)\.json$/i;
// Fields an owner edits as text, in a source list's items.
const TEXT_FIELD =
/^(?:title|text|label|name|caption|description|heading|question|answer|detail|subtitle|summary|part)$/i;
const SCAFFOLD_PROPS = new Set(['id', 'className', 'direction', 'a11y', 'elementProps']);
const PLATFORM_VARIABLE = /^--(wix|wst)-/;
// Style properties the design panel edits; set inline, they win over the panel.
const PANEL_STYLE_KEY =
/\b(background|backgroundColor|color|fontFamily|fontSize|fontWeight|letterSpacing|lineHeight|borderColor|borderRadius|boxShadow)\s*:\s*([^,}]+)/g;
// COMPONENT-CONTRACT.md's default image pool: stock photos for slots the user gave no image for.
const DEFAULT_POOL = [
'11062b_2f97b87dcea2446fa48e9ad9c5457ae1~mv2',
'11062b_73f31c7e7d3544c69dc8ecd8d34c5717~mv2',
'11062b_3682ebfcb08e4da5b3168b62819a1e68~mv2',
'11062b_45e67783d39c4963ab9e4fc418173233~mv2',
'11062b_4c11f014b0d04948b2e6f554076bc40a~mv2',
];
// Read by the editor itself, not by the component.
const SCAFFOLD_VARIABLES = new Set(['--display']);
// Hooks, a React import, or JSX returned from a function. Markup inside strings
// (`innerHTML = '<div>…</div>'`) is how plain logic builds elements, so it doesn't count.
const REACT_MARKERS =
/\buse(?:State|Effect|LayoutEffect|Ref|Memo|Callback|Reducer|Context)\s*\(|\bReact\.\w|from\s+['"]react['"]|(?:return|=>)\s*\(\s*<[A-Za-z>]/;
// Markup the logic builds instead of JSX. Canvas, media and loading helpers aren't visible parts.
const ELEMENT_CREATION = /\bcreateElement\(\s*['"`]([a-z][\w-]*)['"`]/g;
const NON_VISIBLE_TAGS = new Set([
'canvas',
'video',
'audio',
'img',
'source',
'style',
'script',
'link',
'template',
]);
const MARKUP_INJECTION = /\.(?:innerHTML|outerHTML)\s*\+?=\s*[^;]*<|insertAdjacentHTML\s*\(/;
const CANVAS_MARKERS =
/getContext\s*\(|WebGLRenderer|WebGL2RenderingContext|from\s+['"](?:three|p5|pixi\.js|ogl|regl)(?:\/[^'"]*)?['"]|new\s+p5\s*\(|createShader\s*\(/;
const MIN_LINE_LENGTH = 12;
const COPIED_SHARE = 0.3;
// Tailwind's default breakpoints, the widths its sm:/md:/lg:/xl:/2xl: variants start at.
const TAILWIND_WIDTHS = { sm: 640, md: 768, lg: 1024, xl: 1280, '2xl': 1536 };
// The main scaffold file legitimately keeps the source's handlers and effects, so only
// a file that is mostly the source's lines counts as the copied component.
const MAIN_COPIED_SHARE = 0.6;
// Module CSS that styles this many of the source's own global classes is the source's
// stylesheet moved in whole, not rules on the component's parts.
const FOREIGN_GLOBAL_CLASSES = 3;
const USAGE =
'Usage: node <SKILL_ROOT>/scripts/scan-user-source.cjs <component-dir> [--source <file|dir> ...]';
function parseArgs(argv) {
const options = { componentDir: undefined, sources: [] };
for (let i = 0; i < argv.length; i += 1) {
if (argv[i] === '--source') {
if (argv[i + 1] === undefined) throw new Error('--source needs a file or folder');
options.sources.push(argv[i + 1]);
i += 1;
} else if (argv[i].startsWith('--')) {
throw new Error(`Unknown option ${argv[i]}`);
} else if (options.componentDir === undefined) {
options.componentDir = argv[i];
} else {
throw new Error(`Unexpected argument ${argv[i]}`);
}
}
if (!options.componentDir) throw new Error('No component folder given.');
return options;
}
function listFiles(target, extensions) {
if (!fs.existsSync(target)) return [];
const stat = fs.statSync(target);
if (stat.isFile()) return extensions.has(path.extname(target).toLowerCase()) ? [target] : [];
return fs
.readdirSync(target, { withFileTypes: true })
.filter((entry) => entry.name !== 'node_modules' && !entry.name.startsWith('.'))
.flatMap((entry) => listFiles(path.join(target, entry.name), extensions));
}
// File extensions compare in lower case: `Slides.JSON` is a JSON file.
function extOf(file) {
return path.extname(file).toLowerCase();
}
function read(file) {
return fs.readFileSync(file, 'utf8');
}
function significantLines(text) {
return text
.split('\n')
.map((line) => line.trim())
.filter((line) => line.length >= MIN_LINE_LENGTH);
}
// Share of a file's significant lines that appear verbatim in one of the user's files.
function copiedShare(text, sourceLines) {
const lines = significantLines(text);
if (lines.length === 0 || sourceLines.size === 0) return 0;
return lines.filter((line) => sourceLines.has(line)).length / lines.length;
}
function scaffoldFiles(name) {
return {
main: `${name}.tsx`,
props: `${name}.props.ts`,
css: `${name}.module.css`,
fixed: new Set([
'component.tsx',
'component.preview.tsx',
`${name}.tsx`,
`${name}.props.ts`,
`${name}.extension.ts`,
`${name}.generated.ts`,
`${name}.module.css`,
]),
};
}
// Top-level keys of the exported `<Name>Props` object type.
function declaredProps(propsText) {
const start = propsText.search(/export\s+(?:type|interface)\s+\w+Props\b[^{]*\{/);
if (start < 0) return [];
const names = [];
let depth = 0;
let line = '';
for (const char of propsText.slice(propsText.indexOf('{', start))) {
if (char === '{') depth += 1;
if (char === '}') depth -= 1;
if (depth === 0) break;
if (char === '\n') {
// A JSDoc on the prop's own line (`/** @min 0 */ duration?: number;`) is not part of its name.
const match =
depth === 1 &&
/^\s*(?:readonly\s+)?['"]?([A-Za-z_$][\w$]*)['"]?\??\s*:/.exec(
line.replace(/\/\*.*?\*\//g, ''),
);
if (match) names.push(match[1]);
line = '';
} else {
line += char;
}
}
return names;
}
// What each failure means for the site owner, in words they use: no file names,
// props or CSS. The agent quotes these in its final message for what it could not fix.
const OWNER_SENTENCES = {
'copied-react-component':
'The component was added mostly as it was uploaded, so some of its parts can’t be edited from the editor panels yet.',
'unread-prop': (subject) => `The “${subject}” setting doesn’t change the component yet.`,
'dropped-source-option': (subject) =>
`Some options the uploaded component had (such as ${subject}) aren’t in the settings panel yet.`,
'image-without-uri': 'Some images can’t be replaced from the panel yet.',
'source-images-replaced-by-pool':
'The component shows stock photos instead of the images from your upload; you can replace them in the panel.',
'inline-style-blocks-panel': 'Some colors and fonts can’t be changed from the design panel yet.',
'logic-builds-elements':
'Some parts of the component, like its labels and pop-ups, can’t be selected or styled in the editor yet.',
'dropped-responsive-rules':
'Some screen sizes, or dark mode, may look different than in your upload yet.',
'array-prop-without-defaults': (subject) =>
`The “${subject}” list in Manage Items starts empty, though the component shows its items.`,
'source-list-without-array-prop':
'Some lists of items, like its labels or points, can’t be edited from Manage Items yet.',
'view-state-without-element-state':
'The component’s views (like its open view) can’t be designed separately in the editor yet.',
'global-stylesheet': 'Some parts’ styles can’t be changed from the design panel yet.',
'root-display': 'The layout may look different in the editor than in your upload.',
'new-handler-condition':
'Some clicks on the component don’t work the way they did in your upload yet.',
'dropped-source-measurement': 'Some parts may not line up the way they did in your upload yet.',
'fixed-data-attribute':
'Some of the component’s effects may not run the way they did in your upload yet.',
'number-without-range':
'Some number settings have no limits, so they can be set to values that don’t work.',
'color-as-text-prop':
'Some colors are typed as text in the settings panel instead of picked in the design panel.',
'generated-image-without-default':
'The images the component draws itself don’t appear in the panel, so they can’t be replaced yet.',
'undeclared-css-variable': 'Some colors or sizes can’t be changed from the design panel yet.',
'unread-css-variable': 'Some colors in the design panel don’t change the component yet.',
'unregistered-canvas-variable':
'Some colors of the drawing can’t be picked from the design panel yet.',
'no-variable-change-listener':
'Color changes from the design panel show only after the page reloads.',
};
function ownerSentence(check, subject) {
const sentence = OWNER_SENTENCES[check];
return typeof sentence === 'function' ? sentence(humanize(subject ?? '')) : sentence;
}
function humanize(name) {
return name
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[_-]+/g, ' ')
.toLowerCase()
.trim();
}
// The option props (numbers, booleans, fixed choices) a source's `…Props` type
// offers. Content props are often renamed or split in a port; options rarely are.
function sourceOptionProps(text) {
const options = [];
for (const match of text.matchAll(
/(?:export\s+)?(?:interface|type)\s+\w*Props\b[^{=]*=?\s*\{/g,
)) {
let depth = 0;
let line = '';
for (const char of text.slice(match.index + match[0].length - 1)) {
if (char === '{') depth += 1;
if (char === '}') depth -= 1;
if (depth === 0) break;
if (char === '\n') {
const prop =
depth === 1 && /^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\??\s*:\s*([^;/]+)/.exec(line);
if (prop && /^(?:number|boolean|(?:\s*\|?\s*['"][^'"]+['"])+)\s*$/.test(prop[2].trim())) {
options.push(prop[1]);
}
line = '';
} else {
line += char;
}
}
}
return options;
}
// The body of the exported `<Name>Props` type, for checks that look at its fields.
function declaredPropsBlock(propsText) {
const start = propsText.search(/export\s+(?:type|interface)\s+\w+Props\b[^{]*\{/);
if (start < 0) return '';
const open = propsText.indexOf('{', start);
let depth = 0;
for (let i = open; i < propsText.length; i += 1) {
if (propsText[i] === '{') depth += 1;
if (propsText[i] === '}') depth -= 1;
if (depth === 0) return propsText.slice(open, i + 1);
}
return propsText.slice(open);
}
// Top-level fields of the exported `<Name>Props` type, with their written types.
function declaredPropTypes(propsText) {
const fields = [];
let depth = 0;
for (const line of declaredPropsBlock(propsText).split('\n')) {
const match =
depth === 1 &&
/^\s*(?:readonly\s+)?([A-Za-z_$][\w$]*)\??\s*:\s*([^;]+);?/.exec(
line.replace(/\/\*.*?\*\//g, ''),
);
if (match) fields.push({ name: match[1], type: match[2].trim() });
depth += (line.match(/\{/g) ?? []).length - (line.match(/\}/g) ?? []).length;
}
return fields;
}
// Number fields of the `<Name>Props` type with no @min/@max on their line or the one above.
function numberPropsWithoutRange(propsText) {
const lines = declaredPropsBlock(propsText).split('\n');
return lines
.map((line, index) => ({ line, above: lines[index - 1] ?? '' }))
.filter(({ line }) =>
/^\s*(?:readonly\s+)?[A-Za-z_$][\w$]*\??\s*:\s*number\s*;?\s*$/.test(
line.replace(/\/\*\*.*?\*\//, ''),
),
)
.filter(({ line, above }) => !/@(?:min|max)\b/.test(line + above))
.map(({ line }) => /([A-Za-z_$][\w$]*)\??\s*:/.exec(line.replace(/\/\*\*.*?\*\//, ''))[1]);
}
function hexDefault(propsText, name) {
return new RegExp(`\\b${name}\\s*:\\s*['"\`]#[0-9a-f]{3,8}['"\`]`, 'i').test(propsText);
}
// Names of array literals of objects that carry text an owner would edit
// (a title, label or caption with a space in it), e.g. `const DEFAULT_HOTSPOTS = [{ title: 'Weatherproof lid', … }]`.
function textLists(text) {
const lists = [];
for (const match of text.matchAll(
/(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*\[\s*\{/g,
)) {
let depth = 0;
let end = match.index + match[0].lastIndexOf('[');
for (; end < text.length; end += 1) {
if (text[end] === '[') depth += 1;
if (text[end] === ']') depth -= 1;
if (depth === 0) break;
}
const body = text.slice(match.index, end);
if (
/\b(?:title|text|label|name|caption|description|heading|question|answer|detail|subtitle|summary|part|t|s)\s*:\s*['"`][^'"`\n]*\s[^'"`\n]*['"`]/.test(
body,
)
) {
lists.push(match[1]);
}
}
return lists;
}
// Lists in an attached JSON data file (`items.json`, or `{ "slides": [...] }` inside one):
// arrays whose objects carry text an owner would edit.
function jsonTextLists(text, file) {
let data;
try {
data = JSON.parse(text);
} catch {
return [];
}
const lists = [];
const holdsText = (item) =>
item &&
typeof item === 'object' &&
!Array.isArray(item) &&
Object.entries(item).some(
([key, value]) => TEXT_FIELD.test(key) && typeof value === 'string' && value.trim() !== '',
);
const visit = (value, name) => {
if (Array.isArray(value)) {
if (value.some(holdsText)) lists.push(name);
} else if (value && typeof value === 'object') {
for (const [key, field] of Object.entries(value)) visit(field, `${name}.${key}`);
}
};
visit(data, path.basename(file));
return lists;
}
// A prop counts as read when the component takes it from its props, destructured in the
// component's parameters or from `props`, or as `props.name`, and then uses that name. A
// field of something else (`item.title`) or a comment doesn't count.
function propIsRead(prop, code) {
const name = prop.replace(/\$/g, '\\$');
if (
new RegExp(`\\bprops\\??\\.${name}\\b|\\bprops\\[\\s*['"\`]${name}['"\`]\\s*\\]`).test(code)
) {
return true;
}
const destructured = [
...code.matchAll(
/(?:\bfunction\s*[\w$]*\s*\(|=\s*(?:async\s*)?\(|\bforwardRef\(\s*(?:function\s*[\w$]*\s*)?\()\s*\{([^{}]*)\}/g,
),
...code.matchAll(/\{([^{}]*)\}\s*=\s*props\b/g),
];
for (const [, body] of destructured) {
for (const entry of body.split(',')) {
const match = new RegExp(`^\\s*${name}\\s*(?::\\s*([A-Za-z_$][\\w$]*))?\\s*(?:=|$)`).exec(
entry,
);
if (!match) continue;
const local = (match[1] ?? prop).replace(/\$/g, '\\$');
// One match is the destructuring itself. `name:` is an object key, except as the
// middle of a ternary (`dark ? darkImage : image`).
const uses =
code.match(
new RegExp(`(?<![\\w$.])${local}\\b(?!\\s*:)|(?<=\\?\\s*)${local}\\b(?=\\s*:)`, 'g'),
) ?? [];
if (uses.length > 1) return true;
}
}
// Props handed on whole (`{...props}`, `propsRef.current = props`) are read field by field.
return (
/\.\.\.props\b|=\s*props\b|\(\s*props\s*[,)]/.test(code) &&
new RegExp(`\\.${name}\\b`).test(code)
);
}
// The class strings a file renders with: `className="a b"`, `className={'a b'}`, and the
// string arguments of cn/clsx/classnames calls, nested parentheses included.
function classStrings(code) {
const strings = [
...matchAll(code, /\bclassName\s*=\s*["']([^"']+)["']/g),
...matchAll(code, /\bclassName\s*=\s*\{\s*["'`]([^"'`$]+)["'`]\s*\}/g),
];
for (const match of code.matchAll(/\b(?:cn|clsx|cx|classNames|classnames|twMerge)\(/g)) {
let depth = 1;
let end = match.index + match[0].length;
while (end < code.length && depth > 0) {
if (code[end] === '(') depth += 1;
else if (code[end] === ')') depth -= 1;
end += 1;
}
strings.push(
...matchAll(code.slice(match.index + match[0].length, end - 1), /["'`]([^"'`$]+)["'`]/g),
);
}
return strings.map((value) => value.trim().replace(/\s+/g, ' ')).filter(Boolean);
}
function withoutComments(code) {
return code.replace(/\/\*[\s\S]*?\*\//g, '').replace(/(^|[^:\\])\/\/.*$/gm, '$1');
}
function matchAll(text, pattern) {
return [...text.matchAll(pattern)].map((match) => match[1]);
}
function scan({ componentDir, sources }) {
if (!fs.existsSync(componentDir) || !fs.statSync(componentDir).isDirectory()) {
throw new Error(`Component folder not found: ${componentDir}`);
}
const componentName = path.basename(path.resolve(componentDir));
const scaffold = scaffoldFiles(componentName);
const failures = [];
const warnings = [];
const fail = (check, file, step, message, subject) =>
failures.push({ check, file, step, message, owner: ownerSentence(check, subject) });
const warn = (check, file, step, message) => warnings.push({ check, file, step, message });
const sourceFiles = (sources.length ? sources : [DEFAULT_SOURCE_DIR]).flatMap((target) =>
listFiles(target, SOURCE_EXTENSIONS),
);
const sourceLines = new Set(sourceFiles.flatMap((file) => significantLines(read(file))));
const codeFiles = listFiles(componentDir, CODE_EXTENSIONS).filter(
(file) => !file.endsWith('.d.ts') && !file.endsWith('.generated.ts'),
);
const relative = (file) => path.relative(process.cwd(), file);
const texts = new Map(codeFiles.map((file) => [file, read(file)]));
const cssPath = path.join(componentDir, scaffold.css);
const css = fs.existsSync(cssPath) ? read(cssPath) : '';
const mainPath = path.join(componentDir, scaffold.main);
const mainText = texts.get(mainPath) ?? '';
const propsPath = path.join(componentDir, scaffold.props);
const propsText = fs.existsSync(propsPath) ? read(propsPath) : '';
const sourceCode = sourceFiles
.filter((file) => CODE_EXTENSIONS.has(extOf(file)))
.map(read)
.join('\n');
// Step 1: the source's React component is never copied, imported or rendered.
const mainShare = copiedShare(mainText, sourceLines);
if (mainShare >= MAIN_COPIED_SHARE) {
fail(
'copied-react-component',
relative(mainPath),
1,
`${Math.round(mainShare * 100)}% of ${scaffold.main} is the user's code: the source's component was copied into the scaffold file. Rewrite it as the scaffold component: named parts with their global and module classes and elementProps (PARTS.md), the props from ${scaffold.props}, and the source's handlers and effects moved in as they are.`,
);
}
// A file beside the scaffold may hold a sub-component kept as the source wrote it (a canvas
// child with its effect). It fails when it holds the source's exported component, which the
// main file then renders, or when it is the engine copy and still has React in it.
const sourceComponents = [
...matchAll(sourceCode, /export\s+(?:default\s+)?(?:function|class)\s+([A-Z][\w$]*)/g),
...matchAll(sourceCode, /export\s+(?:default\s+)?const\s+([A-Z][\w$]*)\s*[:=]/g),
...matchAll(sourceCode, /export\s+default\s+([A-Z][\w$]*)\s*;?\s*$/gm),
];
for (const [file, text] of texts) {
const base = path.basename(file);
if (scaffold.fixed.has(base)) continue;
const share = copiedShare(text, sourceLines);
const hasReact = REACT_MARKERS.test(text);
const imported = new RegExp(`from\\s+['"]\\./${path.parse(base).name}(?:\\.[jt]sx?)?['"]`).test(
mainText,
);
const holdsSourceComponent = sourceComponents.some((name) =>
new RegExp(`\\b(?:function|class)\\s+${name}\\b|\\bconst\\s+${name}\\s*[:=]`).test(text),
);
if (hasReact && share >= COPIED_SHARE && (holdsSourceComponent || /-engine\./.test(base))) {
fail(
'copied-react-component',
relative(file),
1,
`${Math.round(share * 100)}% of this file is the user's code and it still has React code (hooks or JSX)${
imported ? `, and ${scaffold.main} imports it` : ''
}. Keep only the logic with no hooks or JSX in an engine file, and move the source's component into ${scaffold.main} as the scaffold component (named parts, props from ${scaffold.props}, its hooks and effects as they are); delete this copy.`,
);
} else if (hasReact && /\.[jt]sx$/.test(base)) {
warn(
'extra-react-file',
relative(file),
1,
`A React file outside the scaffold. If it is the source's component or a copy of it, move its markup, props and styles into ${scaffold.main} instead.`,
);
}
}
// Step 3: an inline style beats every design-panel edit of the same property,
// including a design-panel variable the module CSS declares.
const panelVariables = new Set([
...matchAll(css, /(--[\w-]+)\s*:/g),
...matchAll(css, /@property\s+(--[\w-]+)/g),
]);
for (const [file, text] of texts) {
for (const [, body] of text.matchAll(/style=\{\{([^{}]*)\}/g)) {
const blocked = [
...[...body.matchAll(PANEL_STYLE_KEY)]
.filter((match) => !/^\s*['"`]?var\(/.test(match[2]))
.map((match) => match[1]),
...matchAll(body, /['"`](--[\w-]+)['"`]\s*:/g).filter((variable) =>
panelVariables.has(variable),
),
];
if (blocked.length) {
fail(
'inline-style-blocks-panel',
relative(file),
3,
`An inline style sets ${[...new Set(blocked)].join(', ')}, which overrides the design panel. Put the source's values in the part's module CSS rule; for a theme or mode, toggle a class or data attribute and keep each value in CSS.`,
);
}
}
}
// Step 3: styles live in module CSS, never in a plain stylesheet or a <style> tag.
for (const [file, text] of texts) {
const sheets = [
...matchAll(
text,
/import\s+(?:[\w${},\s*]+\s+from\s+)?['"](\.{1,2}\/[^'"?]+\.css)(?:\?[^'"]*)?['"]/g,
),
...matchAll(text, /require\(\s*['"](\.{1,2}\/[^'"?]+\.css)['"]/g),
].filter((sheet) => !sheet.endsWith('.module.css'));
const styleTag = /<style[\s>]/.test(text) || /createElement\(\s*['"`]style['"`]/.test(text);
if (sheets.length || styleTag) {
fail(
'global-stylesheet',
relative(file),
3,
`This file ${[...sheets.map((sheet) => `imports ${sheet}`), ...(styleTag ? ['adds a <style> tag'] : [])].join(' and ')}, which the design panel can't edit. Move each rule into ${scaffold.css}, on the part it styles (a named part's rule, or the root), with the same values, and delete the import or tag.`,
);
}
}
// The source's stylesheet moved into module CSS as :global(.its-classes) is still a stylesheet
// the design panel can't edit: the parts' rules must be on their own module classes.
const foreignGlobals = [
...new Set(
matchAll(css, /:global\(\s*\.([A-Za-z_][\w-]*)/g).filter(
(cls) => cls !== componentName && !cls.startsWith(`${componentName}-`),
),
),
];
if (foreignGlobals.length >= FOREIGN_GLOBAL_CLASSES) {
fail(
'global-stylesheet',
relative(cssPath),
3,
`${scaffold.css} styles the source's own global classes (${foreignGlobals
.slice(0, 6)
.map((cls) => `.${cls}`)
.join(
', ',
)}${foreignGlobals.length > 6 ? ', …' : ''}), so the design panel can't edit them. Move each rule onto the module class of the part it styles, with the same values, and drop the global class.`,
);
}
// Step 3: the root's display is the editor's (`--display`, CSS-GUIDELINES.md); a copied
// `display` on the root rule overrides it and breaks the source's flex or grid layout.
// The root's module class is the first styles.X in the clsx call that starts with the
// component's own class, also after other strings (`clsx('name', 'sh-root', styles.x)`).
const rootCall = new RegExp(`clsx\\(\\s*['"\`]${componentName}['"\`]([^)]*)\\)`).exec(mainText);
const rootClass = /styles\.([A-Za-z_$][\w$]*)/.exec(rootCall?.[1] ?? '')?.[1] ?? 'root';
const rootRuleSetsDisplay = [...css.matchAll(/([^{}]+)\{([^{}]*)\}/g)].some(
([, selector, body]) =>
selector.split(',').some((part) => part.trim() === `.${rootClass}`) &&
/(?:^|[;\s])display\s*:/.test(body),
);
if (rootRuleSetsDisplay) {
fail(
'root-display',
relative(cssPath),
3,
`The .${rootClass} rule sets display directly, which overrides the editor's --display and breaks the layout. Write the source's root display as --display (e.g. --display: flex; flex-direction: column) and keep its other values.`,
);
}
// Step 5: the source's breakpoints and dark mode stay: @container queries at the same widths
// (Tailwind's md: is 768px) and a prefers-color-scheme rule for its dark: variants.
const sourceStyleText = [
sourceCode,
...sourceFiles.filter((file) => !CODE_EXTENSIONS.has(extOf(file))).map(read),
].join('\n');
const sourceTokens = classStrings(withoutComments(sourceCode)).flatMap((value) =>
value.split(' '),
);
const breakpoints = new Set([
...sourceTokens.flatMap((token) => {
const prefix = /^(sm|md|lg|xl|2xl):/.exec(token)?.[1];
return prefix ? [TAILWIND_WIDTHS[prefix]] : [];
}),
...matchAll(sourceStyleText, /@media[^{]*?\((?:min|max)-width\s*:\s*(\d+)px/g).map(Number),
]);
const containerWidths = matchAll(css, /@container[^{]*?\((?:min|max)-width\s*:\s*(\d+)px/g).map(
Number,
);
const missingWidths = [...breakpoints].filter(
(width) => !containerWidths.some((query) => Math.abs(query - width) <= 1),
);
const darkInSource =
sourceTokens.some((token) => token.startsWith('dark:')) ||
/prefers-color-scheme\s*:\s*dark/.test(sourceStyleText);
const darkInPort =
/prefers-color-scheme\s*:\s*dark/.test(css) ||
/['"]dark['"]/.test(declaredPropsBlock(propsText));
const droppedRules = [
...(missingWidths.length
? [
`${missingWidths.map((width) => `${width}px`).join(', ')} breakpoint${missingWidths.length > 1 ? 's' : ''}`,
]
: []),
...(darkInSource && !darkInPort ? ['dark-mode colors'] : []),
];
if (droppedRules.length) {
fail(
'dropped-responsive-rules',
relative(cssPath),
5,
`The port drops the source's ${droppedRules.join(' and ')}. Write each source breakpoint as a @container query at the same width with the source's values (Tailwind sm/md/lg/xl/2xl are 640/768/1024/1280/1536px), and its dark: variants or dark @media rules as a @media (prefers-color-scheme: dark) rule.`,
);
}
for (const sheet of matchAll(css, /@import\s+(?:url\()?['"]?(\.{1,2}\/[^'")\s]+\.css)/g)) {
fail(
'global-stylesheet',
relative(cssPath),
3,
`${scaffold.css} imports ${sheet}, whose rules the design panel can't edit. Move them into ${scaffold.css} on the parts they style, and delete the import.`,
);
}
// Step 4: a view or mode the whole component switches into is a root ElementState.
if (!/\bElementState\b/.test(declaredPropsBlock(propsText))) {
const views = [];
for (const [file, text] of texts) {
const base = path.basename(file);
if (!/\.[jt]sx$/.test(base) || base === 'component.tsx' || base === 'component.preview.tsx')
continue;
for (const match of text.matchAll(
/const\s+\[\s*([A-Za-z_$][\w$]*)\s*,\s*set\w*\s*\]\s*=\s*(?:React\.)?useState(?:<[^>]*>)?\(\s*([^)]*)\)/g,
)) {
const [, state, initial] = match;
const isMode =
/^['"`]/.test(initial.trim()) && /mode|view|screen|layout|display/i.test(state);
const isView =
/^(?:true|false)$/.test(initial.trim()) &&
/open|expand|explod|flip|detail|collaps|zoom|fullscreen|reveal/i.test(state);
if (isMode || isView) views.push(state);
}
}
if (views.length) {
fail(
'view-state-without-element-state',
relative(propsPath),
4,
`The component switches views with ${[...new Set(views)].join(', ')} in useState, and ${scaffold.props} has no ElementState prop, so the editor has no state to design it in. Add a root ElementState prop per DESIGN-STATES.md and keep the state in sync with it.`,
);
}
}
// Step 7: every declared prop is read by the component.
const readers = [...texts]
.filter(([file]) => {
const base = path.basename(file);
return (
base !== scaffold.props &&
base !== 'component.tsx' &&
base !== 'component.preview.tsx' &&
!base.endsWith('.extension.ts')
);
})
.map(([, text]) => text)
.join('\n');
const readerCode = withoutComments(readers);
for (const prop of declaredProps(propsText)) {
if (SCAFFOLD_PROPS.has(prop)) continue;
if (!propIsRead(prop, readerCode)) {
fail(
'unread-prop',
relative(propsPath),
7,
`"${prop}" is declared but nothing reads it. Wire it to what it controls, or remove it if the source has no such option.`,
prop,
);
}
}
// Step 2: visible elements are JSX named parts, never markup the logic builds.
for (const [file, text] of texts) {
const built = [
...matchAll(text, ELEMENT_CREATION).filter((tag) => !NON_VISIBLE_TAGS.has(tag.toLowerCase())),
...(MARKUP_INJECTION.test(text) ? ['innerHTML'] : []),
];
if (built.length) {
fail(
'logic-builds-elements',
relative(file),
2,
`This file builds visible markup itself (${[...new Set(built)].join(', ')}), so the editor can't select or style it. Render each element (hotspots, tooltips, labels, slides) in JSX as a named part per PARTS.md, and have the logic find it through a data-* attribute and only position, show or update it.`,
);
}
}
// Step 2: every list of items the source holds becomes an array prop.
const listSources = [
...sourceFiles.filter((file) => CODE_EXTENSIONS.has(extOf(file))).map(read),
...[...texts]
.filter(([file]) => !scaffold.fixed.has(path.basename(file)))
.map(([, text]) => text),
];
const sourceLists = [
...new Set([
...listSources.flatMap(textLists),
...sourceFiles
.filter((file) => extOf(file) === '.json' && !CONFIG_JSON.test(path.basename(file)))
.flatMap((file) => jsonTextLists(read(file), file)),
]),
];
if (sourceLists.length && !/\bArray<|\[\]\s*;/.test(declaredPropsBlock(propsText))) {
fail(
'source-list-without-array-prop',
relative(propsPath),
2,
`The source holds ${sourceLists.join(', ')} as lists of items, and ${scaffold.props} has no array prop. Make each an Array<{…}> prop whose defaults are the source's items, so Manage Items can edit them, and have the logic read the list from props.`,
);
}
// An array prop with no defaults starts Manage Items empty, while the code still shows the
// source's own list as a fallback (`hotspotList || DEFAULT_HOTSPOTS`).
const defaultsBlock = propsText.slice(Math.max(0, propsText.search(/\bdefaultProps\b/)));
const emptyLists = sourceLists.length
? declaredPropTypes(propsText)
.filter(({ type }) => /\bArray</.test(type) || /\[\]\s*$/.test(type))
.map(({ name }) => name)
.filter((name) => !new RegExp(`\\b${name}\\s*:`).test(defaultsBlock))
: [];
if (emptyLists.length) {
fail(
'array-prop-without-defaults',
relative(propsPath),
2,
`${emptyLists.join(', ')} ${emptyLists.length > 1 ? 'have' : 'has'} no defaults, so Manage Items starts empty while the code falls back to the source's own list. Put the source's items in defaultProps and have the code read only the prop.`,
emptyLists[0],
);
}
// A list of texts written into the component's own JSX files is never editable.
for (const [file, text] of texts) {
const base = path.basename(file);
if (!/\.[jt]sx$/.test(base) || base === 'component.tsx' || base === 'component.preview.tsx')
continue;
const lists = textLists(text);
if (lists.length) {
fail(
'source-list-without-array-prop',
relative(file),
2,
`${lists.join(', ')} ${lists.length > 1 ? 'are lists' : 'is a list'} of texts written into the component, so the owner can't edit them. Move each into ${scaffold.props} as an Array<{…}> prop (or text props) with these values as defaults.`,
);
}
}
// Step 3: numbers carry their range (COMPONENT-CONTRACT.md: @min/@max).
const unranged = numberPropsWithoutRange(propsText);
if (unranged.length) {
fail(
'number-without-range',
relative(propsPath),
3,
`${unranged.join(', ')} ${unranged.length > 1 ? 'are numbers' : 'is a number'} without @min/@max. Add the range the source allows (its clamp, slider or docs) in the unit the owner thinks in.`,
);
}
// Step 2: colors are design-panel variables, not text the owner types.
const colorProps = declaredPropTypes(propsText)
.filter(
({ name, type }) =>
/^string$/.test(type) && (/colou?r/i.test(name) || hexDefault(propsText, name)),
)
.map(({ name }) => name);
if (colorProps.length) {
fail(
'color-as-text-prop',
relative(propsPath),
2,
`${colorProps.join(', ')} ${colorProps.length > 1 ? 'are colors' : 'is a color'} typed as a string prop, a text field in the settings panel. Make each a CSS custom property declared on the root (registered with @property on a canvas) that the code reads, so the owner picks it in the design panel.`,
);
}
// Step 3: every option the source offers stays a prop ("never drop it").
const portedProps = declaredProps(propsText).map((name) => name.toLowerCase());
const dropped = [
...new Set(
sourceFiles
.filter((file) => CODE_EXTENSIONS.has(extOf(file)))
.flatMap((file) => sourceOptionProps(read(file))),
),
].filter((name) => {
const lower = name.toLowerCase();
return !portedProps.some((ported) => ported.includes(lower) || lower.includes(ported));
});
if (dropped.length) {
fail(
'dropped-source-option',
relative(propsPath),
3,
`The source offers ${dropped.join(', ')} as options, and ${scaffold.props} has none of them. Keep each as a prop with the source's default (numbers with @min/@max), and pass it to what it controls; never hardcode it.`,
dropped.slice(0, 3).map(humanize).join(', '),
);
}
// Step 6: the source's own images (painted or linked) are the defaults, never the stock pool.
const PAINTS = /\.(?:toDataURL|toBlob)\s*\(|URL\.createObjectURL\s*\(/;
const sourceMarkup = sourceFiles
.filter((file) => !CODE_EXTENSIONS.has(extOf(file)) && extOf(file) !== '.json')
.map(read)
.join('\n');
const sourceText = `${sourceCode}\n${sourceMarkup}`;
// Images the source has: painted ones, attached image files, imported or `url()` files, and
// image URLs, also extensionless CDN links when they sit in a src, an image field or `url()`.
const sourceHasImages =
PAINTS.test(sourceCode) ||
(sources.length ? sources : [DEFAULT_SOURCE_DIR]).some(
(target) => listFiles(target, IMAGE_EXTENSIONS).length > 0,
) ||
/https?:\/\/[^\s'"`)]+\.(?:png|jpe?g|webp|gif|avif)\b/i.test(sourceText) ||
/(?:\bfrom\s*|\brequire\(\s*|\burl\(\s*)['"]?[^'"()\s]+\.(?:png|jpe?g|webp|gif|avif)\b/i.test(
sourceText,
) ||
/(?:\bsrc\s*[=:]\s*\{?\s*|\b(?:image|img|photo|picture|poster|thumbnail|backgroundImage)\s*:\s*|\burl\(\s*)['"`]?https?:\/\//i.test(
sourceText,
);
const poolDefaults = DEFAULT_POOL.filter((file) => propsText.includes(file));
if (sourceHasImages && poolDefaults.length) {
fail(
'source-images-replaced-by-pool',
relative(propsPath),
6,
`The source has its own images, but ${poolDefaults.length} default${poolDefaults.length > 1 ? 's come' : ' comes'} from the contract's stock pool. Upload the source's images (render the ones it paints with render-images.cjs) and use those as the defaults. If uploading or rendering one fails, keeping the pool image for it is fine: say so in your final message with the script's error.`,
);
}
// Step 6: every Image default is a Wix Media file.
for (const literal of propsText.match(/\{[^{}]*\burl\s*:[^{}]*\}/g) ?? []) {
if (!/\buri\s*:\s*['"`][^'"`]+['"`]/.test(literal)) {
fail(
'image-without-uri',
relative(propsPath),
6,
`This default has a url and no uri: ${literal.replace(/\s+/g, ' ').slice(0, 120)}. Upload it with upload-media.cjs and use the { uri, url } it prints.`,
);
}
}
// Step 6: images the code generates become uploaded defaults too, also when the port
// dropped the painter (a CSS gradient in its place): the source still paints them.
const allCode = [...texts.values()].join('\n');
const hasImageField = /\bImage\b/.test(propsText.replace(/import[^;]*;/g, ''));
// A Wix Media file ID counts however it is written (`uri: '…'` or a helper like `image('…', alt)`).
const hasImageDefault = /\buri\s*:\s*['"`][^'"`]+['"`]|\b[0-9a-f]{6}_[0-9a-f]{32}~mv2\.\w+/.test(
propsText,
);
if (hasImageField && !hasImageDefault && (PAINTS.test(allCode) || PAINTS.test(sourceCode))) {
fail(
'generated-image-without-default',
relative(propsPath),
6,
`The ${PAINTS.test(allCode) ? 'code generates' : 'source paints'} its images at runtime (toDataURL, toBlob or createObjectURL), and no Image default has a uri, so the panel shows no images. Copy the painter from the source unchanged into the engine file and export it (step 1), render each default from there with render-images.cjs, upload the files with upload-media.cjs, and use the results as the Image defaults; keep the generator for items left without an image. Never replace the painter with CSS or a new drawing.`,
);
}
// Step 3: the source's handlers move as they are; an added element stops its own events.
// `event.target === ref.current`, `e.target !== e.currentTarget`, either way round.
const TARGET = String.raw`\b(?:event|e|evt|ev)\.target\b`;
const OTHER = String.raw`(?:\b[\w$]+\.current\b|\b(?:event|e|evt|ev)\.currentTarget\b)`;
const targetGuard = new RegExp(
`${TARGET}\\s*[!=]==?\\s*${OTHER}|${OTHER}\\s*[!=]==?\\s*${TARGET}`,
);
if (!targetGuard.test(withoutComments(sourceCode))) {
for (const [file, text] of texts) {
if (!targetGuard.test(withoutComments(text))) continue;
fail(
'new-handler-condition',
relative(file),
3,
"A handler checks event.target against a ref, which the source never does, so clicks on the component's children (its slides, images or text) are ignored. Keep the source's handler as it is, and have the element you added (the play button) call event.stopPropagation() in its own onPointerDown, onPointerUp and onClick.",
);
}
}
// Step 3: a source that measures its parts to place them (the arc picker's anchor and
// selection frame) loses its layout when a port replaces the measuring with fixed CSS.
const MEASURES = {
ResizeObserver: /\bResizeObserver\b/,
offsetWidth: /\.offsetWidth\b/,
clientWidth: /\.clientWidth\b/,
getBoundingClientRect: /\.getBoundingClientRect\s*\(/,
useLayoutEffect: /\buseLayoutEffect\b/,
};
const portCode = withoutComments(allCode);
const sourceMeasures = Object.keys(MEASURES).filter((name) =>
MEASURES[name].test(withoutComments(sourceCode)),
);
if (sourceMeasures.length && !Object.values(MEASURES).some((pattern) => pattern.test(portCode))) {
fail(
'dropped-source-measurement',
relative(mainPath),
3,
`The source measures its parts to place them (${sourceMeasures.join(', ')}), and the port measures nothing, so its layout comes from fixed values instead. Port the source's measuring code and what it positions as they are, with only prop and ref names changed.`,
);
}
// Step 3: an attribute only the source's code sets (`root.dataset.gl = 'on'`) starts unset;
// a fixed value in the port's JSX (`data-gl="off"`) trips the source's own checks of it.
const sourceLogic = withoutComments(sourceCode);
for (const [file, text] of texts) {
const fixed = new Map();
for (const match of withoutComments(text).matchAll(
/\s(data-[a-z][\w-]*)=(?:"([^"]*)"|'([^']*)'|\{\s*(['"`])([^'"`]*)\4\s*\})/g,
)) {
fixed.set(match[1], match[2] ?? match[3] ?? match[5]);
}
for (const [attribute, value] of fixed) {
const key = attribute.slice(5).replace(/-([a-z])/g, (_, letter) => letter.toUpperCase());
const setInJsx = new RegExp(`\\s${attribute}=`).test(sourceLogic);
const setByCode = new RegExp(
`\\.dataset\\.${key}\\b|\\.dataset\\[\\s*['"\`]${key}['"\`]|(?:get|set|remove|toggle|has)Attribute\\(\\s*['"\`]${attribute}['"\`]`,
).test(sourceLogic);
if (setInJsx || !setByCode) continue;
fail(
'fixed-data-attribute',
relative(file),
3,
`The JSX sets ${attribute}="${value}", which the source never does: only the source's code sets ${attribute} (dataset.${key}), and its checks expect it to start unset. Remove ${attribute}="${value}" from the JSX and keep the source's code as it is.`,
);
}
}
// Step 2: CSS variables the code or CSS reads are declared on the root.
const declaredInCss = new Set(matchAll(css, /(--[\w-]+)\s*:/g));
const registered = new Set(matchAll(css, /@property\s+(--[\w-]+)/g));
const setByCode = new Set([
...matchAll(allCode, /['"`](--[\w-]+)['"`]\s*:/g),
...matchAll(allCode, /setProperty\(\s*['"`](--[\w-]+)/g),
]);
// A read through a variable (`getPropertyValue(token)`) reads the names the code spells out.
const readsByName = /getPropertyValue\(\s*[A-Za-z_$`]/.test(allCode);
const readByCode = new Set([
...matchAll(allCode, /getPropertyValue\(\s*['"`](--[\w-]+)/g),
...(readsByName
? matchAll(allCode, /['"`](--[\w-]+)['"`]/g).filter((name) => !setByCode.has(name))
: []),
]);
const readByCss = new Set(matchAll(css, /var\(\s*(--[\w-]+)/g));
for (const variable of readByCode) {
if (PLATFORM_VARIABLE.test(variable) || declaredInCss.has(variable)) continue;
fail(
'undeclared-css-variable',
relative(cssPath),
2,
`The code reads ${variable}, but ${scaffold.css} never declares it on the root${registered.has(variable) ? ' (an @property rule alone is not a declaration)' : ''}. Declare it on the root with the source's value, so it shows in the design panel.`,
);
}
for (const variable of readByCss) {
if (
PLATFORM_VARIABLE.test(variable) ||
declaredInCss.has(variable) ||
registered.has(variable) ||
setByCode.has(variable)
)
continue;
fail(
'undeclared-css-variable',
relative(cssPath),
2,
`The CSS reads ${variable}, but nothing declares or sets it.`,
);
}
const mentionedByCode = new Set(matchAll(allCode, /['"`](--[\w-]+)['"`]/g));
for (const variable of declaredInCss) {
if (PLATFORM_VARIABLE.test(variable) || SCAFFOLD_VARIABLES.has(variable)) continue;
if (readByCss.has(variable) || readByCode.has(variable) || mentionedByCode.has(variable))
continue;
fail(
'unread-css-variable',
relative(cssPath),
2,
`${variable} is declared, but neither the CSS nor the code reads it, so changing it in the design panel does nothing. Read it where the source uses that value${
CANVAS_MARKERS.test(allCode)
? ' (on a canvas, through the cached reader in CANVAS-COMPONENTS.md)'
: ''
}, or remove it.`,
);
}
// CANVAS-COMPONENTS.md: values a canvas draws with are registered, and edits are heard.
if (CANVAS_MARKERS.test(allCode)) {
for (const variable of readByCode) {
if (PLATFORM_VARIABLE.test(variable) || registered.has(variable)) continue;
fail(
'unregistered-canvas-variable',
relative(cssPath),
2,
`The canvas reads ${variable}, but it has no @property rule, so the design panel can't type it. Register it per CANVAS-COMPONENTS.md.`,
);
}
// An edit is heard only with both: a transitionend listener, and a transition on the variable.
const listens = /\baddEventListener\(\s*['"`]transitionend['"`]|\bonTransitionEnd\s*=/.test(
withoutComments(allCode),
);
const transitioned = new Set(
matchAll(css, /transition(?:-property)?\s*:([^;}]*)/g).flatMap(
(value) => value.match(/--[\w-]+/g) ?? [],
),
);
const heard = [...readByCode].some((variable) => transitioned.has(variable));
if (readByCode.size > 0 && (!listens || !heard)) {
fail(
'no-variable-change-listener',
relative(mainPath),
2,
`The canvas reads CSS variables, but ${
listens ? '' : 'nothing calls addEventListener("transitionend")'
}${!listens && !heard ? ' and ' : ''}${
heard ? '' : 'no CSS transition names the variables it reads'
}, so a design-panel edit does not repaint it. Add the 1ms transition on those variables and the transitionend listener with the cached reader from CANVAS-COMPONENTS.md.`,
);
}
if (readByCode.size === 0) {
warn(
'no-design-panel-variables',
relative(cssPath),
2,
'The component draws on a canvas but reads no CSS variables, so the owner cannot change the colors or fonts it draws in the design panel. If the source draws with colors or fonts, expose them per CANVAS-COMPONENTS.md.',
);
}
}
if (sourceFiles.length === 0) {
warn(
'no-source-files',
DEFAULT_SOURCE_DIR,
1,
'No user files found, so copied code could not be compared. Pass --source <file> for pasted code you saved.',
);
}
return {
summary: { failures: failures.length, warnings: warnings.length },
// For the final message: one plain sentence per kind of failure left unfixed.
tellTheOwner: [...new Set(failures.map((failure) => failure.owner).filter(Boolean))],
failures,
warnings,
exitCode: failures.length ? 1 : 0,
};
}
function main() {
let options;
try {
options = parseArgs(process.argv.slice(2));
} catch (error) {
console.error(`${error.message}\n${USAGE}`);
process.exit(2);
}
try {
const { exitCode, ...report } = scan(options);
console.log(JSON.stringify(report, null, 2));
process.exit(exitCode);
} catch (error) {
console.error(error.message);
process.exit(2);
}
}
module.exports = { scan, parseArgs, declaredProps, copiedShare, significantLines };
if (require.main === module) main();
Requirements and compatibility
Works with: Claude Code, Claude apps, OpenAI Codex, GitHub Copilot, Cursor, Gemini CLI, Kiro, OpenCode, Goose, Amp, Roo Code, JetBrains Junie, Factory Droid, Devin (ex-Windsurf), Mistral Vibe, OpenHands, Letta
“Works with” means the tool reads the open Agent Skills format; a skill written for one tool may use features others lack.
Needs: Needs Node.js
The author says: requires `@wix/cli` >= 1.1.192.
Security
What our scanner noticed· Low risk
- Scripts that your agent may run
This skill includes scripts that your agent may run. Read them before you use it.
A static scan is not a guarantee. Skills are third-party content: review a skill before you let an agent use it.
Attribution and licence
- Published by
- Wix
- Publisher
- Wix (Official)
- Source
- wix/skills · skills/wix-app · commit d168bc2
- Licence
- MIT License (redistribution allowed)
- LICENSE (added by AutoGineer from LICENSE)
- ATTRIBUTION.md (written by AutoGineer)
- Synced
AutoGineer is not affiliated with or endorsed by Wix.
Download ZIP
The ZIP holds the skill’s files, its licence files and an ATTRIBUTION.md. Download wix-app.zip