@uimatch/selector-anchors
Selector resolution plugin for uiMatch using AST-based anchors.
Installation
Install both @uimatch/selector-anchors (this package) and @uimatch/selector-spi:
npm install @uimatch/selector-anchors @uimatch/selector-spi
# or
pnpm add @uimatch/selector-anchors @uimatch/selector-spi
# or
bun add @uimatch/selector-anchors @uimatch/selector-spi
Requirements:
- Node.js:
>=20.19or>=22.12 - Bun: Compatible with Bun 1.x (all scripts use Bun)
- TypeScript: This package includes TypeScript as a runtime dependency for AST-based selector resolution
- Why runtime dependency? TypeScript is required at runtime for AST parsing and analysis, not just for type checking
- Module System: ESM only (CommonJS is not supported)
- Dynamic import is supported:
import('@uimatch/selector-anchors') require()will not work
- Dynamic import is supported:
Overview
This package provides intelligent selector resolution by analyzing source code (TypeScript/JSX/HTML) and maintaining anchor points that survive code refactoring and line number changes.
System Flow
flowchart LR
CLI["uimatch compare (plugin invocation)"] --> PL["@uimatch/selector-anchors (SPI)"]
PL --> A0["Load anchors.json"]
A0 --> M1["selectBestAnchor(initialSelector)"]
M1 --> Q1{"snippetHash exists?"}
Q1 -- Yes --> S1["findSnippetMatch(±radius, fuzzy)"]
Q1 -- No --> AST
S1 --> AST["AST/HTML resolution (tiered fallback)"]
AST --> C1["Candidate selectors"]
C1 --> L1["Liveness check via probe.check()"]
L1 --> SC["calculateStabilityScore()"]
SC --> P1["Select best selector"]
P1 --> W1{"writeBack enabled?"}
W1 -- Yes --> U1["Update anchors.json resolvedCss/lastSeen/lastKnown"]
P1 --> R1["Resolution { selector, score, reasons }"]
R1 --> CORE["To @uimatch/core compareImages()"]
Features
- AST-based Resolution: Extract semantic selectors from TypeScript/JSX and HTML
- Snippet Hash Matching: Detect code movement using fuzzy matching
- Liveness Checking: Verify selectors work in the browser
- Stability Scoring: Calculate selector quality (0-100)
- SPI Compliance: Pluggable architecture via SPI interface
Health Check
The plugin provides a healthCheck() method to verify runtime dependencies:
- TypeScript compilation is required (returns
healthy=falseiftscfails) parse5(HTML parsing) is optional by default (warnings are logged buthealthy=true)- When
UIMATCH_HEALTHCHECK_STRICT_HTML=trueis set,parse5becomes required andhealthy=falsewill be returned if unavailable
This allows TypeScript-only projects to use the plugin without HTML parsing capabilities.
Usage
CLI Tool
The package provides a command-line tool for adding anchors to anchors.json:
# Add a new anchor
npx uimatch-anchors --file src/components/Button.tsx --line 10 --column 2 --id button-root
# Overwrite existing anchor
npx uimatch-anchors --file src/components/Button.tsx --line 10 --column 2 --id button-root --force
# Custom output file
npx uimatch-anchors --file src/components/Button.tsx --line 10 --column 2 --id button-root --output custom.json
# Show help
npx uimatch-anchors --help
Note: The CLI automatically generates snippet hashes from the specified source code location. Once anchors are created, you should commit anchors.json to your repository for team collaboration.
As a Plugin (Phase 3+)
uimatch compare \
--selectors anchors.json \
--selectors-plugin @uimatch/selector-anchors
Direct Usage
import plugin from '@uimatch/selector-anchors';
const resolution = await plugin.resolve({
url: 'http://localhost:3000',
initialSelector: '.my-button',
anchorsPath: './anchors.json',
probe: myProbeImplementation,
});
console.log(resolution.selector); // Best selector found
console.log(resolution.stabilityScore); // Quality score 0-100
console.log(resolution.reasons); // Selection reasoning
How It Works
- Load anchors JSON (source locations + hints)
- Match code snippets (fuzzy match if code moved)
- Extract selectors from AST/HTML
- Verify liveness via Probe interface
- Score stability and return best match
Fuzzy Matching: When original line number no longer matches, searches nearby lines in the same file using partial match scoring (80% token match + 20% char match). Default threshold: 0.6 (configurable via UIMATCH_SNIPPET_FUZZY_THRESHOLD). Exact match only (without original snippet).
Stability Scoring
Stability scores (0-100) are calculated using weighted components:
graph LR
HQ["Hint quality<br/>(testid/role/text/css)"] --> SUM[Weighted Sum]
SM[Snippet match] --> SUM
LV["Liveness<br/>(probe.check)"] --> SUM
SP["Specificity<br/>(data-testid > role > id > text)"] --> SUM
SUM --> SCORE["Stability (0-100)"]
Default Weights:
- Hint Quality (0.4): Strategy preference quality (testid=1.0, role=0.8, text=0.5, css=0.3)
- Snippet Match (0.2): Whether code snippet hash matched (1.0=matched, 0.0=not matched)
- Liveness (0.3): Browser validation result (1.0=alive, 0.5=not checked, 0.0=dead)
- Specificity (0.1): Selector specificity (data-testid=1.0, role[name]=0.9, id=0.6, text[5-24]=0.6, ...)
Custom Weights:
You can customize weights via the options parameter:
const resolution = await plugin.resolve({
// ... other options
stabilityScoreOptions: {
weights: {
hintQuality: 0.3,
snippetMatch: 0.3,
liveness: 0.3,
specificity: 0.1,
},
},
});
Environment Variable Configuration:
For production tuning, weights can be adjusted via environment variables:
export UIMATCH_STABILITY_HINT_WEIGHT=0.3
export UIMATCH_STABILITY_SNIPPET_WEIGHT=0.3
export UIMATCH_STABILITY_LIVENESS_WEIGHT=0.3
export UIMATCH_STABILITY_SPECIFICITY_WEIGHT=0.1
These variables are checked at runtime and override default values, enabling post-deployment tuning without code changes.
Weight Normalization:
The plugin automatically normalizes weights to ensure they sum to 1.0. You can specify any positive numbers, and they will be proportionally adjusted:
# These weights (sum=10) will be normalized to (0.4, 0.2, 0.3, 0.1)
export UIMATCH_STABILITY_HINT_WEIGHT=4
export UIMATCH_STABILITY_SNIPPET_WEIGHT=2
export UIMATCH_STABILITY_LIVENESS_WEIGHT=3
export UIMATCH_STABILITY_SPECIFICITY_WEIGHT=1
Priority: Programmatic options (via stabilityScoreOptions) > Environment variables > Default values
Configuration
Timeout Settings
The plugin uses configurable timeouts for various operations. You can adjust these via environment variables:
AST Parsing Timeouts:
The AST resolution uses a tiered fallback strategy with three timeout levels:
flowchart TB
START[resolveFromTypeScript] --> FAST["Fast path (tag / data-testid / id)\n< 300ms"]
FAST -- "Candidates found" --> DONE[return selectors]
FAST -- "Empty/timeout" --> ATTR["Attr-only (all attrs, no text)\n< 600ms"]
ATTR -- "Candidates found" --> DONE
ATTR -- "Empty/timeout" --> FULL["Full parse (including text)\n< 900ms"]
FULL -- "Candidates found" --> DONE
FULL -- "Empty/timeout" --> H["Heuristics (regex-based)\n~ 50ms"]
H --> DONE
# Fast path timeout (tag + data-testid/id only) - default: 300ms
export UIMATCH_AST_FAST_PATH_TIMEOUT_MS=300
# Attribute-only parsing timeout (all attributes, no text) - default: 600ms
export UIMATCH_AST_ATTR_TIMEOUT_MS=600
# Full parse timeout (everything including text) - default: 900ms
export UIMATCH_AST_FULL_TIMEOUT_MS=900
Other Timeouts:
# Liveness probe timeout - default: 600ms
export UIMATCH_PROBE_TIMEOUT_MS=600
# HTML parsing timeout - default: 300ms
export UIMATCH_HTML_PARSE_TIMEOUT_MS=300
# Snippet hash matching timeout - default: 50ms
export UIMATCH_SNIPPET_MATCH_TIMEOUT_MS=50
Snippet Matching Configuration:
# Maximum search radius for snippet matching (lines) - default: 400
export UIMATCH_SNIPPET_MAX_RADIUS=400
# High confidence threshold for early exit (0.0-1.0) - default: 0.92
export UIMATCH_SNIPPET_HIGH_CONFIDENCE=0.92
# Fuzzy matching threshold (0.0-1.0) - default: 0.6
export UIMATCH_SNIPPET_FUZZY_THRESHOLD=0.6
Debug Logging:
Enable debug logging via the DEBUG environment variable:
# Enable all uimatch debug logs
export DEBUG=uimatch:*
# Enable only selector-anchors logs
export DEBUG=uimatch:selector-anchors
Integration Notes
Text Matching (uiMatch Plugin): Text matching verification is provided by @uimatch/cli in /uiMatch compare (via the textCheck option).
mode: 'self' | 'descendants' / normalize: 'none' | 'nfkc' | 'nfkc_ws' / match: 'exact' | 'contains' | 'ratio' / minRatio: 0.98.
Role Selector Resolution: role:button[name="Submit"] uses getByRole(). Boolean attributes like checked/selected/... fall back to CSS.
Anchors JSON Format
JSON Schema Support
Enable IDE autocompletion and validation by adding the $schema property to your anchors.json:
{
"$schema": "https://unpkg.com/@uimatch/selector-anchors@latest/dist/schema/anchors.schema.json",
"version": "1.0.0",
"anchors": []
}
VS Code/IntelliJ IDEA/WebStorm will automatically provide:
- ✅ Autocomplete for all anchor properties
- ✅ Inline documentation for each field
- ✅ Real-time validation errors
- ✅ Schema-driven snippets
Local Schema (Alternative):
{
"$schema": "./node_modules/@uimatch/selector-anchors/dist/schema/anchors.schema.json",
"version": "1.0.0",
"anchors": []
}
Minimal Example
The simplest anchors.json with required fields only:
{
"version": "1.0.0",
"anchors": [
{
"id": "button-primary",
"source": {
"file": "src/components/Button.tsx",
"line": 42,
"col": 10
}
}
]
}
Standard Example (Recommended)
Full-featured anchors.json with hints, snippet hash, and metadata:
{
"version": "1.0.0",
"anchors": [
{
"id": "button-primary",
"source": {
"file": "src/components/Button.tsx",
"line": 42,
"col": 10
},
"hint": {
"prefer": ["testid", "role", "text"],
"testid": "button-primary",
"role": "button",
"expectedText": "Submit"
},
"snippetHash": "a3f2c9d8e1",
"snippet": "export function Button({ variant = 'primary' }) {\n return (\n <button data-testid=\"button-primary\" role=\"button\">\n Submit\n </button>\n );\n}",
"snippetContext": {
"contextBefore": 3,
"contextAfter": 3,
"algorithm": "sha1",
"hashDigits": 10
},
"subselector": "button[data-testid='button-primary']",
"resolvedCss": "button[data-testid='button-primary']",
"lastSeen": "2024-01-15T10:30:00Z",
"meta": {
"component": "Button",
"description": "Primary action button in header",
"tags": ["interactive", "form"]
}
},
{
"id": "card-title",
"source": {
"file": "src/components/Card.tsx",
"line": 18,
"col": 6
},
"hint": {
"prefer": ["role", "text"],
"role": "heading",
"expectedText": "Product Title"
},
"snippetHash": "b7e4d2c1f9",
"snippetContext": {
"contextBefore": 2,
"contextAfter": 2,
"algorithm": "sha1",
"hashDigits": 10
},
"meta": {
"component": "Card",
"description": "Card heading element",
"tags": ["typography", "heading"]
}
}
]
}
Field Notes:
snippetHash: Auto-generated hash for code movement detection (fuzzy matching when line numbers change)snippetContext: Controls snippet extraction window (default: ±3 lines, sha1, 10 digits)subselector: Optional child element selector for Figma auto-ROI targetingresolvedCss: Last resolved CSS selector (write-back cache for fast lookup)lastSeen: Timestamp when the selector was last successfully resolvedmeta: Human-readable metadata for organization and debugging
For complete schema details, see schema.ts.
Architecture
Anchor Matching System
- Multi-criteria scoring (exact match, testid, role, component metadata, snippet hash, stability)
- Best match selection from multiple anchors
AST Resolution
- TypeScript/JSX parsing with fallback strategies
- Selector extraction with stability scoring
- Snippet hash generation for code movement detection
Integration
- SPI-compliant plugin architecture
- CLI integration via
--selectors-pluginflag - Probe interface for liveness checking
- Write-back support for anchor updates
Path Aliases: #anchors/* resolved via esbuild (build) and imports field (runtime)
License
MIT
@uimatch/selector-anchors
Selector resolution plugin for uiMatch using AST-based anchors
Example
import plugin from '@uimatch/selector-anchors';
// Use the plugin
const result = await plugin.resolve({
initialSelector: '[data-testid="button"]',
anchorsPath: './anchors.json',
probe: async (selector) => ({ isAlive: true, selector })
});
Interfaces
- AnchorScore
- ASTResolverResult
- FallbackSelectorResult
- HealthCheckResult
- HTMLResolverResult
- Probe
- ProbeOptions
- ProbeResult
- Resolution
- ResolveContext
- SelectorResolverPlugin
- SnippetHashOptions
- SnippetHashResult
- StabilityScore
- StabilityScoreOptions
Type Aliases
- Fallbacks
- Hints
- Metadata
- ResolvedAnchor
- SelectorAnchor
- SelectorHint
- SelectorsAnchors
- SnippetContext
- SourceLocation
Variables
- default
- DEFAULT_SNIPPET_CONFIG
- DEFAULT_TIMEOUTS
- ENV_VARS
- FallbacksSchema
- HintsSchema
- MetadataSchema
- ResolvedAnchorSchema
- SelectorAnchorSchema
- SelectorHintSchema
- SelectorsAnchorsSchema
- SnippetContextSchema
- SourceLocationSchema
Functions
- calculateStabilityScore
- checkLivenessAll
- checkLivenessPriority
- clearFileCache
- compareStabilityScores
- createEmptyAnchors
- defaultPostWrite
- findMostStableSelector
- findSnippetMatch
- generateFallbackSelectors
- generatePlaywrightFallbacks
- generateSnippetHash
- getConfig
- isLive
- isSelectorResolverPlugin
- loadSelectorsAnchors
- matchAnchors
- performHealthCheck
- resolve
- resolveFromHTML
- resolveFromTypeScript
- resolveProjectPath
- saveSelectorsAnchors
- selectBestAnchor