@amplitude/element-selector
Advanced tools
| /** | ||
| * Config resolver — collapses the optional, string-based remote-config payload | ||
| * into the fully-typed `ResolvedSelectorConfig` that strategies, orchestrator, | ||
| * and fallback consume at runtime. | ||
| * | ||
| * Responsibilities: | ||
| * | ||
| * 1. Apply defaults for every field. The defaults (enabled = false, | ||
| * attribute = `data-amp-track-id`, built-in pattern packs) match the | ||
| * design doc's "v1 defaults" table. | ||
| * | ||
| * 2. Compile regex strings into `RegExp[]`. Bad regexes are skipped by the | ||
| * underlying `compile` helpers; when a logger is provided it surfaces a | ||
| * warning for each invalid pattern so customers can diagnose why their | ||
| * override isn't taking effect. | ||
| * | ||
| * 3. Treat customer-provided pattern lists as a **full replacement** of the | ||
| * defaults — not a merge. This matches the design doc: "The customer | ||
| * sees the defaults in the remote-config UI and may add or remove | ||
| * patterns as needed." If a customer provides `autogeneratedIdPatterns: | ||
| * []`, that's their explicit request to disable autogen filtering. | ||
| * | ||
| * 4. Clamp `maxAncestorWalkDepth` into a sane range. Negative / zero values | ||
| * are coerced to `undefined` (treat as "no limit") rather than throwing — | ||
| * remote config should never crash the engine. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { ElementSelectorLogger, ElementSelectorRemoteConfig, ResolvedSelectorConfig } from '../types'; | ||
| /** | ||
| * Built-in defaults applied when a field is absent from the remote payload. | ||
| * Exported so consumers (and tests) can introspect the baseline without | ||
| * round-tripping through `resolveSelectorConfig({})`. | ||
| */ | ||
| export declare const DEFAULT_RESOLVED_CONFIG: Readonly<ResolvedSelectorConfig>; | ||
| /** | ||
| * Resolve a (possibly partial, possibly absent) remote-config payload into a | ||
| * fully-typed runtime config. Always returns a fresh object; never mutates the | ||
| * input. | ||
| * | ||
| * Field-by-field semantics: | ||
| * | ||
| * - `enabled`: defaults to false (the engine ships dormant). Any boolean in | ||
| * the payload — including `false` — wins. | ||
| * - `explicitTrackingAttribute`: defaults to `data-amp-track-id`. Empty | ||
| * strings are rejected (they'd make the strategy match every element); | ||
| * fall back to the default in that case. | ||
| * - `autogeneratedIdPatterns` / `unstableClassPatterns`: present → compile | ||
| * the strings (skipping invalid regexes), use the compiled list as-is. | ||
| * Absent → use the defaults. | ||
| * - `maxAncestorWalkDepth`: positive finite integer → use as-is. Anything | ||
| * else (zero, negative, NaN, Infinity) → `undefined` (no limit). | ||
| */ | ||
| export declare function resolveSelectorConfig(remote?: ElementSelectorRemoteConfig, logger?: ElementSelectorLogger): ResolvedSelectorConfig; | ||
| //# sourceMappingURL=resolve-config.d.ts.map |
| {"version":3,"file":"resolve-config.d.ts","sourceRoot":"","sources":["../../../src/config/resolve-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,qBAAqB,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAOtG;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,sBAAsB,CAQnE,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,2BAA2B,EACpC,MAAM,CAAC,EAAE,qBAAqB,GAC7B,sBAAsB,CA4BxB"} |
| "use strict"; | ||
| /** | ||
| * Config resolver — collapses the optional, string-based remote-config payload | ||
| * into the fully-typed `ResolvedSelectorConfig` that strategies, orchestrator, | ||
| * and fallback consume at runtime. | ||
| * | ||
| * Responsibilities: | ||
| * | ||
| * 1. Apply defaults for every field. The defaults (enabled = false, | ||
| * attribute = `data-amp-track-id`, built-in pattern packs) match the | ||
| * design doc's "v1 defaults" table. | ||
| * | ||
| * 2. Compile regex strings into `RegExp[]`. Bad regexes are skipped by the | ||
| * underlying `compile` helpers; when a logger is provided it surfaces a | ||
| * warning for each invalid pattern so customers can diagnose why their | ||
| * override isn't taking effect. | ||
| * | ||
| * 3. Treat customer-provided pattern lists as a **full replacement** of the | ||
| * defaults — not a merge. This matches the design doc: "The customer | ||
| * sees the defaults in the remote-config UI and may add or remove | ||
| * patterns as needed." If a customer provides `autogeneratedIdPatterns: | ||
| * []`, that's their explicit request to disable autogen filtering. | ||
| * | ||
| * 4. Clamp `maxAncestorWalkDepth` into a sane range. Negative / zero values | ||
| * are coerced to `undefined` (treat as "no limit") rather than throwing — | ||
| * remote config should never crash the engine. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.resolveSelectorConfig = exports.DEFAULT_RESOLVED_CONFIG = void 0; | ||
| var tslib_1 = require("tslib"); | ||
| var autogenerated_ids_1 = require("../patterns/autogenerated-ids"); | ||
| var unstable_classes_1 = require("../patterns/unstable-classes"); | ||
| /** | ||
| * Built-in defaults applied when a field is absent from the remote payload. | ||
| * Exported so consumers (and tests) can introspect the baseline without | ||
| * round-tripping through `resolveSelectorConfig({})`. | ||
| */ | ||
| exports.DEFAULT_RESOLVED_CONFIG = Object.freeze({ | ||
| // v1 ships disabled — autocapture only flips to the new engine when the | ||
| // remote config explicitly enables it for an org. | ||
| enabled: false, | ||
| explicitTrackingAttribute: 'data-amp-track-id', | ||
| autogeneratedIdPatterns: tslib_1.__spreadArray([], tslib_1.__read(autogenerated_ids_1.DEFAULT_AUTOGENERATED_ID_PATTERNS), false), | ||
| unstableClassPatterns: tslib_1.__spreadArray([], tslib_1.__read(unstable_classes_1.DEFAULT_UNSTABLE_CLASS_PATTERNS), false), | ||
| maxAncestorWalkDepth: undefined, | ||
| }); | ||
| /** | ||
| * Resolve a (possibly partial, possibly absent) remote-config payload into a | ||
| * fully-typed runtime config. Always returns a fresh object; never mutates the | ||
| * input. | ||
| * | ||
| * Field-by-field semantics: | ||
| * | ||
| * - `enabled`: defaults to false (the engine ships dormant). Any boolean in | ||
| * the payload — including `false` — wins. | ||
| * - `explicitTrackingAttribute`: defaults to `data-amp-track-id`. Empty | ||
| * strings are rejected (they'd make the strategy match every element); | ||
| * fall back to the default in that case. | ||
| * - `autogeneratedIdPatterns` / `unstableClassPatterns`: present → compile | ||
| * the strings (skipping invalid regexes), use the compiled list as-is. | ||
| * Absent → use the defaults. | ||
| * - `maxAncestorWalkDepth`: positive finite integer → use as-is. Anything | ||
| * else (zero, negative, NaN, Infinity) → `undefined` (no limit). | ||
| */ | ||
| function resolveSelectorConfig(remote, logger) { | ||
| if (!remote) { | ||
| // Return a fresh copy so callers can safely mutate the result without | ||
| // poisoning the frozen defaults singleton. | ||
| return cloneDefaults(); | ||
| } | ||
| var resolved = cloneDefaults(); | ||
| if (typeof remote.enabled === 'boolean') { | ||
| resolved.enabled = remote.enabled; | ||
| } | ||
| if (typeof remote.explicitTrackingAttribute === 'string' && remote.explicitTrackingAttribute !== '') { | ||
| resolved.explicitTrackingAttribute = remote.explicitTrackingAttribute; | ||
| } | ||
| if (Array.isArray(remote.autogeneratedIdPatterns)) { | ||
| resolved.autogeneratedIdPatterns = (0, autogenerated_ids_1.compile)(remote.autogeneratedIdPatterns, logger); | ||
| } | ||
| if (Array.isArray(remote.unstableClassPatterns)) { | ||
| resolved.unstableClassPatterns = (0, unstable_classes_1.compile)(remote.unstableClassPatterns, logger); | ||
| } | ||
| resolved.maxAncestorWalkDepth = clampDepth(remote.maxAncestorWalkDepth); | ||
| return resolved; | ||
| } | ||
| exports.resolveSelectorConfig = resolveSelectorConfig; | ||
| /** | ||
| * Coerce a remote-supplied depth value into the runtime contract: | ||
| * - undefined / non-number / NaN / Infinity / <= 0 → undefined (no limit) | ||
| * - positive finite number → floor()'d integer | ||
| */ | ||
| function clampDepth(depth) { | ||
| if (typeof depth !== 'number') | ||
| return undefined; | ||
| if (!Number.isFinite(depth)) | ||
| return undefined; | ||
| if (depth <= 0) | ||
| return undefined; | ||
| return Math.floor(depth); | ||
| } | ||
| function cloneDefaults() { | ||
| return { | ||
| enabled: exports.DEFAULT_RESOLVED_CONFIG.enabled, | ||
| explicitTrackingAttribute: exports.DEFAULT_RESOLVED_CONFIG.explicitTrackingAttribute, | ||
| autogeneratedIdPatterns: tslib_1.__spreadArray([], tslib_1.__read(exports.DEFAULT_RESOLVED_CONFIG.autogeneratedIdPatterns), false), | ||
| unstableClassPatterns: tslib_1.__spreadArray([], tslib_1.__read(exports.DEFAULT_RESOLVED_CONFIG.unstableClassPatterns), false), | ||
| maxAncestorWalkDepth: exports.DEFAULT_RESOLVED_CONFIG.maxAncestorWalkDepth, | ||
| }; | ||
| } | ||
| //# sourceMappingURL=resolve-config.js.map |
| {"version":3,"file":"resolve-config.js","sourceRoot":"","sources":["../../../src/config/resolve-config.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;;;AAGH,mEAGuC;AACvC,iEAAwH;AAExH;;;;GAIG;AACU,QAAA,uBAAuB,GAAqC,MAAM,CAAC,MAAM,CAAC;IACrF,wEAAwE;IACxE,kDAAkD;IAClD,OAAO,EAAE,KAAK;IACd,yBAAyB,EAAE,mBAAmB;IAC9C,uBAAuB,2CAAM,qDAAiC,SAAC;IAC/D,qBAAqB,2CAAM,kDAA+B,SAAC;IAC3D,oBAAoB,EAAE,SAAS;CAChC,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,qBAAqB,CACnC,MAAoC,EACpC,MAA8B;IAE9B,IAAI,CAAC,MAAM,EAAE;QACX,sEAAsE;QACtE,2CAA2C;QAC3C,OAAO,aAAa,EAAE,CAAC;KACxB;IAED,IAAM,QAAQ,GAA2B,aAAa,EAAE,CAAC;IAEzD,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE;QACvC,QAAQ,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;KACnC;IAED,IAAI,OAAO,MAAM,CAAC,yBAAyB,KAAK,QAAQ,IAAI,MAAM,CAAC,yBAAyB,KAAK,EAAE,EAAE;QACnG,QAAQ,CAAC,yBAAyB,GAAG,MAAM,CAAC,yBAAyB,CAAC;KACvE;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,uBAAuB,CAAC,EAAE;QACjD,QAAQ,CAAC,uBAAuB,GAAG,IAAA,2BAA8B,EAAC,MAAM,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;KAC3G;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,EAAE;QAC/C,QAAQ,CAAC,qBAAqB,GAAG,IAAA,0BAA4B,EAAC,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;KACrG;IAED,QAAQ,CAAC,oBAAoB,GAAG,UAAU,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAExE,OAAO,QAAQ,CAAC;AAClB,CAAC;AA/BD,sDA+BC;AAED;;;;GAIG;AACH,SAAS,UAAU,CAAC,KAAyB;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9C,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACjC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,aAAa;IACpB,OAAO;QACL,OAAO,EAAE,+BAAuB,CAAC,OAAO;QACxC,yBAAyB,EAAE,+BAAuB,CAAC,yBAAyB;QAC5E,uBAAuB,2CAAM,+BAAuB,CAAC,uBAAuB,SAAC;QAC7E,qBAAqB,2CAAM,+BAAuB,CAAC,qBAAqB,SAAC;QACzE,oBAAoB,EAAE,+BAAuB,CAAC,oBAAoB;KACnE,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Config resolver — collapses the optional, string-based remote-config payload\n * into the fully-typed `ResolvedSelectorConfig` that strategies, orchestrator,\n * and fallback consume at runtime.\n *\n * Responsibilities:\n *\n * 1. Apply defaults for every field. The defaults (enabled = false,\n * attribute = `data-amp-track-id`, built-in pattern packs) match the\n * design doc's \"v1 defaults\" table.\n *\n * 2. Compile regex strings into `RegExp[]`. Bad regexes are skipped by the\n * underlying `compile` helpers; when a logger is provided it surfaces a\n * warning for each invalid pattern so customers can diagnose why their\n * override isn't taking effect.\n *\n * 3. Treat customer-provided pattern lists as a **full replacement** of the\n * defaults — not a merge. This matches the design doc: \"The customer\n * sees the defaults in the remote-config UI and may add or remove\n * patterns as needed.\" If a customer provides `autogeneratedIdPatterns:\n * []`, that's their explicit request to disable autogen filtering.\n *\n * 4. Clamp `maxAncestorWalkDepth` into a sane range. Negative / zero values\n * are coerced to `undefined` (treat as \"no limit\") rather than throwing —\n * remote config should never crash the engine.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ElementSelectorRemoteConfig, ResolvedSelectorConfig } from '../types';\nimport {\n DEFAULT_AUTOGENERATED_ID_PATTERNS,\n compile as compileAutogeneratedIdPatterns,\n} from '../patterns/autogenerated-ids';\nimport { DEFAULT_UNSTABLE_CLASS_PATTERNS, compile as compileUnstableClassPatterns } from '../patterns/unstable-classes';\n\n/**\n * Built-in defaults applied when a field is absent from the remote payload.\n * Exported so consumers (and tests) can introspect the baseline without\n * round-tripping through `resolveSelectorConfig({})`.\n */\nexport const DEFAULT_RESOLVED_CONFIG: Readonly<ResolvedSelectorConfig> = Object.freeze({\n // v1 ships disabled — autocapture only flips to the new engine when the\n // remote config explicitly enables it for an org.\n enabled: false,\n explicitTrackingAttribute: 'data-amp-track-id',\n autogeneratedIdPatterns: [...DEFAULT_AUTOGENERATED_ID_PATTERNS],\n unstableClassPatterns: [...DEFAULT_UNSTABLE_CLASS_PATTERNS],\n maxAncestorWalkDepth: undefined,\n});\n\n/**\n * Resolve a (possibly partial, possibly absent) remote-config payload into a\n * fully-typed runtime config. Always returns a fresh object; never mutates the\n * input.\n *\n * Field-by-field semantics:\n *\n * - `enabled`: defaults to false (the engine ships dormant). Any boolean in\n * the payload — including `false` — wins.\n * - `explicitTrackingAttribute`: defaults to `data-amp-track-id`. Empty\n * strings are rejected (they'd make the strategy match every element);\n * fall back to the default in that case.\n * - `autogeneratedIdPatterns` / `unstableClassPatterns`: present → compile\n * the strings (skipping invalid regexes), use the compiled list as-is.\n * Absent → use the defaults.\n * - `maxAncestorWalkDepth`: positive finite integer → use as-is. Anything\n * else (zero, negative, NaN, Infinity) → `undefined` (no limit).\n */\nexport function resolveSelectorConfig(\n remote?: ElementSelectorRemoteConfig,\n logger?: ElementSelectorLogger,\n): ResolvedSelectorConfig {\n if (!remote) {\n // Return a fresh copy so callers can safely mutate the result without\n // poisoning the frozen defaults singleton.\n return cloneDefaults();\n }\n\n const resolved: ResolvedSelectorConfig = cloneDefaults();\n\n if (typeof remote.enabled === 'boolean') {\n resolved.enabled = remote.enabled;\n }\n\n if (typeof remote.explicitTrackingAttribute === 'string' && remote.explicitTrackingAttribute !== '') {\n resolved.explicitTrackingAttribute = remote.explicitTrackingAttribute;\n }\n\n if (Array.isArray(remote.autogeneratedIdPatterns)) {\n resolved.autogeneratedIdPatterns = compileAutogeneratedIdPatterns(remote.autogeneratedIdPatterns, logger);\n }\n\n if (Array.isArray(remote.unstableClassPatterns)) {\n resolved.unstableClassPatterns = compileUnstableClassPatterns(remote.unstableClassPatterns, logger);\n }\n\n resolved.maxAncestorWalkDepth = clampDepth(remote.maxAncestorWalkDepth);\n\n return resolved;\n}\n\n/**\n * Coerce a remote-supplied depth value into the runtime contract:\n * - undefined / non-number / NaN / Infinity / <= 0 → undefined (no limit)\n * - positive finite number → floor()'d integer\n */\nfunction clampDepth(depth: number | undefined): number | undefined {\n if (typeof depth !== 'number') return undefined;\n if (!Number.isFinite(depth)) return undefined;\n if (depth <= 0) return undefined;\n return Math.floor(depth);\n}\n\nfunction cloneDefaults(): ResolvedSelectorConfig {\n return {\n enabled: DEFAULT_RESOLVED_CONFIG.enabled,\n explicitTrackingAttribute: DEFAULT_RESOLVED_CONFIG.explicitTrackingAttribute,\n autogeneratedIdPatterns: [...DEFAULT_RESOLVED_CONFIG.autogeneratedIdPatterns],\n unstableClassPatterns: [...DEFAULT_RESOLVED_CONFIG.unstableClassPatterns],\n maxAncestorWalkDepth: DEFAULT_RESOLVED_CONFIG.maxAncestorWalkDepth,\n };\n}\n"]} |
| /** | ||
| * Engine factory — composes the strategy chain (via `runOrchestrator`) with | ||
| * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine` | ||
| * interface. | ||
| * | ||
| * This is the surface every consumer talks to: | ||
| * | ||
| * - autocapture SDK plugin → instantiates one engine per init() call, | ||
| * wires it into the click handler, and forwards remote-config updates | ||
| * via `updateConfig`. | ||
| * - app.amplitude.com tagging UI → fetches the customer's remote config | ||
| * out-of-band and stands up a transient engine to compute selectors for | ||
| * elements the user clicks in the iframe. | ||
| * - Chrome extension visual tagger → reads the customer's already-live | ||
| * engine off `window.amplitude.elementSelector` (when present) and | ||
| * subscribes to `onConfigChange` so the extension's preview stays in | ||
| * sync with the customer's runtime config. | ||
| * | ||
| * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather | ||
| * than the raw remote payload — config resolution is a separate concern handled | ||
| * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets | ||
| * dashboard / extension consumers stand up an engine from a static snapshot | ||
| * without re-running the full resolver. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { ElementSelectorLogger, ResolvedSelectorConfig, SelectorEngine } from './types'; | ||
| import { OrchestratorOptions } from './orchestrator'; | ||
| export interface CreateSelectorEngineOptions { | ||
| /** Optional document or shadow root for uniqueness checks. Defaults per-call to the target's owner document. */ | ||
| scope?: ParentNode; | ||
| /** Optional override of the strategy chain. Primarily for testing / dashboard ad-hoc runs. */ | ||
| strategies?: OrchestratorOptions['strategies']; | ||
| /** | ||
| * Optional logger threaded through the orchestrator and the subscriber-fan-out | ||
| * inside `updateConfig`. When provided, the engine surfaces malformed | ||
| * selectors (`debug`) and listener exceptions (`warn`); when absent it stays | ||
| * silent — preserving the legacy "fire-and-forget" semantics existing | ||
| * consumers may rely on. | ||
| */ | ||
| logger?: ElementSelectorLogger; | ||
| } | ||
| /** | ||
| * Build a `SelectorEngine` bound to the supplied config. | ||
| * | ||
| * The returned engine is independent — calling the factory twice yields two | ||
| * engines with separate config state and separate subscriber lists. This is the | ||
| * shape the autocapture plugin wants (one engine per SDK instance) and the | ||
| * shape the Chrome extension consumes off the page (the extension reads, never | ||
| * writes). | ||
| */ | ||
| export declare function createSelectorEngine(initialConfig: ResolvedSelectorConfig, options?: CreateSelectorEngineOptions): SelectorEngine; | ||
| //# sourceMappingURL=engine.d.ts.map |
| {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAmB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAGtE,MAAM,WAAW,2BAA2B;IAC1C,gHAAgH;IAChH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,8FAA8F;IAC9F,UAAU,CAAC,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;IAC/C;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,sBAAsB,EACrC,OAAO,GAAE,2BAAgC,GACxC,cAAc,CAkDhB"} |
| "use strict"; | ||
| /** | ||
| * Engine factory — composes the strategy chain (via `runOrchestrator`) with | ||
| * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine` | ||
| * interface. | ||
| * | ||
| * This is the surface every consumer talks to: | ||
| * | ||
| * - autocapture SDK plugin → instantiates one engine per init() call, | ||
| * wires it into the click handler, and forwards remote-config updates | ||
| * via `updateConfig`. | ||
| * - app.amplitude.com tagging UI → fetches the customer's remote config | ||
| * out-of-band and stands up a transient engine to compute selectors for | ||
| * elements the user clicks in the iframe. | ||
| * - Chrome extension visual tagger → reads the customer's already-live | ||
| * engine off `window.amplitude.elementSelector` (when present) and | ||
| * subscribes to `onConfigChange` so the extension's preview stays in | ||
| * sync with the customer's runtime config. | ||
| * | ||
| * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather | ||
| * than the raw remote payload — config resolution is a separate concern handled | ||
| * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets | ||
| * dashboard / extension consumers stand up an engine from a static snapshot | ||
| * without re-running the full resolver. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.createSelectorEngine = void 0; | ||
| var tslib_1 = require("tslib"); | ||
| var orchestrator_1 = require("./orchestrator"); | ||
| var fallback_css_path_1 = require("./fallback-css-path"); | ||
| /** | ||
| * Build a `SelectorEngine` bound to the supplied config. | ||
| * | ||
| * The returned engine is independent — calling the factory twice yields two | ||
| * engines with separate config state and separate subscriber lists. This is the | ||
| * shape the autocapture plugin wants (one engine per SDK instance) and the | ||
| * shape the Chrome extension consumes off the page (the extension reads, never | ||
| * writes). | ||
| */ | ||
| function createSelectorEngine(initialConfig, options) { | ||
| if (options === void 0) { options = {}; } | ||
| var config = initialConfig; | ||
| var subscribers = new Set(); | ||
| var logger = options.logger; | ||
| var orchestratorOptions = { | ||
| strategies: options.strategies, | ||
| scope: options.scope, | ||
| logger: logger, | ||
| }; | ||
| return { | ||
| generate: function (el) { | ||
| var _a, _b; | ||
| // Try the strategy chain first. | ||
| var composed = (0, orchestrator_1.runOrchestrator)(el, config, orchestratorOptions); | ||
| if (composed !== null) { | ||
| return composed; | ||
| } | ||
| // Strategy chain found nothing usable — fall back to the hardened | ||
| // positional walker with the same scope used by the orchestrator. | ||
| return (0, fallback_css_path_1.fallbackCssPath)(el, config, { scope: (_b = (_a = options.scope) !== null && _a !== void 0 ? _a : el.ownerDocument) !== null && _b !== void 0 ? _b : document }); | ||
| }, | ||
| getConfig: function () { | ||
| return config; | ||
| }, | ||
| updateConfig: function (next) { | ||
| var e_1, _a; | ||
| config = next; | ||
| try { | ||
| // Notify subscribers in insertion order. Errors thrown by individual | ||
| // subscribers are isolated so one bad listener can't break the others — | ||
| // the extension's listener is the canonical consumer here and we don't | ||
| // want SDK-side changes to cascade-fail because of an extension bug. | ||
| for (var subscribers_1 = tslib_1.__values(subscribers), subscribers_1_1 = subscribers_1.next(); !subscribers_1_1.done; subscribers_1_1 = subscribers_1.next()) { | ||
| var cb = subscribers_1_1.value; | ||
| try { | ||
| cb(next); | ||
| } | ||
| catch (e) { | ||
| var message = e instanceof Error ? e.message : String(e); | ||
| logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: onConfigChange subscriber threw \u2014 ".concat(message)); | ||
| } | ||
| } | ||
| } | ||
| catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
| finally { | ||
| try { | ||
| if (subscribers_1_1 && !subscribers_1_1.done && (_a = subscribers_1.return)) _a.call(subscribers_1); | ||
| } | ||
| finally { if (e_1) throw e_1.error; } | ||
| } | ||
| }, | ||
| onConfigChange: function (cb) { | ||
| subscribers.add(cb); | ||
| return function () { | ||
| subscribers.delete(cb); | ||
| }; | ||
| }, | ||
| }; | ||
| } | ||
| exports.createSelectorEngine = createSelectorEngine; | ||
| //# sourceMappingURL=engine.js.map |
| {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;;;;AAGH,+CAAsE;AACtE,yDAAsD;AAiBtD;;;;;;;;GAQG;AACH,SAAgB,oBAAoB,CAClC,aAAqC,EACrC,OAAyC;IAAzC,wBAAA,EAAA,YAAyC;IAEzC,IAAI,MAAM,GAA2B,aAAa,CAAC;IACnD,IAAM,WAAW,GAAG,IAAI,GAAG,EAA4C,CAAC;IACxE,IAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE9B,IAAM,mBAAmB,GAAwB;QAC/C,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,QAAA;KACP,CAAC;IAEF,OAAO;QACL,QAAQ,YAAC,EAAW;;YAClB,gCAAgC;YAChC,IAAM,QAAQ,GAAG,IAAA,8BAAe,EAAC,EAAE,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC;YAClE,IAAI,QAAQ,KAAK,IAAI,EAAE;gBACrB,OAAO,QAAQ,CAAC;aACjB;YACD,kEAAkE;YAClE,kEAAkE;YAClE,OAAO,IAAA,mCAAe,EAAC,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAA,MAAA,OAAO,CAAC,KAAK,mCAAI,EAAE,CAAC,aAAa,mCAAI,QAAQ,EAAE,CAAC,CAAC;QAC/F,CAAC;QAED,SAAS;YACP,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,YAAY,YAAC,IAA4B;;YACvC,MAAM,GAAG,IAAI,CAAC;;gBACd,qEAAqE;gBACrE,wEAAwE;gBACxE,uEAAuE;gBACvE,qEAAqE;gBACrE,KAAiB,IAAA,gBAAA,iBAAA,WAAW,CAAA,wCAAA,iEAAE;oBAAzB,IAAM,EAAE,wBAAA;oBACX,IAAI;wBACF,EAAE,CAAC,IAAI,CAAC,CAAC;qBACV;oBAAC,OAAO,CAAC,EAAE;wBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;wBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,8EAAkE,OAAO,CAAE,CAAC,CAAC;qBAC3F;iBACF;;;;;;;;;QACH,CAAC;QAED,cAAc,YAAC,EAA4C;YACzD,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACpB,OAAO;gBACL,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACzB,CAAC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC;AArDD,oDAqDC","sourcesContent":["/**\n * Engine factory — composes the strategy chain (via `runOrchestrator`) with\n * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine`\n * interface.\n *\n * This is the surface every consumer talks to:\n *\n * - autocapture SDK plugin → instantiates one engine per init() call,\n * wires it into the click handler, and forwards remote-config updates\n * via `updateConfig`.\n * - app.amplitude.com tagging UI → fetches the customer's remote config\n * out-of-band and stands up a transient engine to compute selectors for\n * elements the user clicks in the iframe.\n * - Chrome extension visual tagger → reads the customer's already-live\n * engine off `window.amplitude.elementSelector` (when present) and\n * subscribes to `onConfigChange` so the extension's preview stays in\n * sync with the customer's runtime config.\n *\n * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather\n * than the raw remote payload — config resolution is a separate concern handled\n * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets\n * dashboard / extension consumers stand up an engine from a static snapshot\n * without re-running the full resolver.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { runOrchestrator, OrchestratorOptions } from './orchestrator';\nimport { fallbackCssPath } from './fallback-css-path';\n\nexport interface CreateSelectorEngineOptions {\n /** Optional document or shadow root for uniqueness checks. Defaults per-call to the target's owner document. */\n scope?: ParentNode;\n /** Optional override of the strategy chain. Primarily for testing / dashboard ad-hoc runs. */\n strategies?: OrchestratorOptions['strategies'];\n /**\n * Optional logger threaded through the orchestrator and the subscriber-fan-out\n * inside `updateConfig`. When provided, the engine surfaces malformed\n * selectors (`debug`) and listener exceptions (`warn`); when absent it stays\n * silent — preserving the legacy \"fire-and-forget\" semantics existing\n * consumers may rely on.\n */\n logger?: ElementSelectorLogger;\n}\n\n/**\n * Build a `SelectorEngine` bound to the supplied config.\n *\n * The returned engine is independent — calling the factory twice yields two\n * engines with separate config state and separate subscriber lists. This is the\n * shape the autocapture plugin wants (one engine per SDK instance) and the\n * shape the Chrome extension consumes off the page (the extension reads, never\n * writes).\n */\nexport function createSelectorEngine(\n initialConfig: ResolvedSelectorConfig,\n options: CreateSelectorEngineOptions = {},\n): SelectorEngine {\n let config: ResolvedSelectorConfig = initialConfig;\n const subscribers = new Set<(config: ResolvedSelectorConfig) => void>();\n const logger = options.logger;\n\n const orchestratorOptions: OrchestratorOptions = {\n strategies: options.strategies,\n scope: options.scope,\n logger,\n };\n\n return {\n generate(el: Element): string {\n // Try the strategy chain first.\n const composed = runOrchestrator(el, config, orchestratorOptions);\n if (composed !== null) {\n return composed;\n }\n // Strategy chain found nothing usable — fall back to the hardened\n // positional walker with the same scope used by the orchestrator.\n return fallbackCssPath(el, config, { scope: options.scope ?? el.ownerDocument ?? document });\n },\n\n getConfig(): Readonly<ResolvedSelectorConfig> {\n return config;\n },\n\n updateConfig(next: ResolvedSelectorConfig): void {\n config = next;\n // Notify subscribers in insertion order. Errors thrown by individual\n // subscribers are isolated so one bad listener can't break the others —\n // the extension's listener is the canonical consumer here and we don't\n // want SDK-side changes to cascade-fail because of an extension bug.\n for (const cb of subscribers) {\n try {\n cb(next);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: onConfigChange subscriber threw — ${message}`);\n }\n }\n },\n\n onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n };\n}\n"]} |
| /** | ||
| * Fallback CSS-path walker — the safety net for the v1 element-selector | ||
| * algorithm. | ||
| * | ||
| * When the orchestrator's strategy chain finds nothing usable across the full | ||
| * ancestor walk, the engine invokes this fallback. It produces a positional | ||
| * selector by walking up from the target to <html>, expressing each step as | ||
| * either an id anchor (when one survives the autogen filter) or a | ||
| * `tag:nth-of-type(n)` step. | ||
| * | ||
| * Key differences from the legacy autocapture `cssPath`: | ||
| * | ||
| * 1. Ids are filtered through `getStableId`, which honors the | ||
| * explicit-tracking-attribute suppression signal AND the autogenerated-id | ||
| * pattern pack. The legacy `cssPath` would happily anchor on `id=":r5:"` | ||
| * and produce selectors that never match across page loads. | ||
| * | ||
| * 2. Classes — when they're used at all for sibling disambiguation — are | ||
| * filtered through `filterClasses` against the unstable-class pattern | ||
| * pack. The current v1 implementation here doesn't emit classes (we lean | ||
| * entirely on `:nth-of-type` for sibling disambiguation), but the | ||
| * filtering helper is wired through so future iterations can opt back in | ||
| * surgically without re-deriving the policy. | ||
| * | ||
| * 3. A unique anchor id terminates the walk early — once we hit `body#main`, | ||
| * we don't need to keep walking up to `<html>`. | ||
| * | ||
| * 4. Ambiguous id anchors are skipped. Duplicate ids are invalid HTML but | ||
| * common enough in the wild that fallback must not emit selectors that | ||
| * resolve to the wrong target. | ||
| * | ||
| * Output format mirrors the orchestrator's output: `<anchor> > <descent>`. | ||
| * When no usable id is found anywhere on the walk, the fallback emits a | ||
| * pure-positional selector rooted at `html`. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { ResolvedSelectorConfig } from './types'; | ||
| export interface FallbackCssPathOptions { | ||
| /** Document or shadow root used for uniqueness checks. Defaults to the target's owner document. */ | ||
| scope?: ParentNode; | ||
| } | ||
| /** | ||
| * Build a positional CSS selector for `el`, using stable ids as anchors when | ||
| * available and falling back to `tag:nth-of-type(n)` for disambiguation. | ||
| * | ||
| * Honors `config.maxAncestorWalkDepth` defensively — once the depth limit is | ||
| * reached, the walker stops and returns whatever it has built so far rooted at | ||
| * the deepest reached ancestor. | ||
| */ | ||
| export declare function fallbackCssPath(el: Element, config: ResolvedSelectorConfig, options?: FallbackCssPathOptions): string; | ||
| //# sourceMappingURL=fallback-css-path.d.ts.map |
| {"version":3,"file":"fallback-css-path.d.ts","sourceRoot":"","sources":["../../src/fallback-css-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAIjD,MAAM,WAAW,sBAAsB;IACrC,mGAAmG;IACnG,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,sBAAsB,EAC9B,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAgCR"} |
| "use strict"; | ||
| /** | ||
| * Fallback CSS-path walker — the safety net for the v1 element-selector | ||
| * algorithm. | ||
| * | ||
| * When the orchestrator's strategy chain finds nothing usable across the full | ||
| * ancestor walk, the engine invokes this fallback. It produces a positional | ||
| * selector by walking up from the target to <html>, expressing each step as | ||
| * either an id anchor (when one survives the autogen filter) or a | ||
| * `tag:nth-of-type(n)` step. | ||
| * | ||
| * Key differences from the legacy autocapture `cssPath`: | ||
| * | ||
| * 1. Ids are filtered through `getStableId`, which honors the | ||
| * explicit-tracking-attribute suppression signal AND the autogenerated-id | ||
| * pattern pack. The legacy `cssPath` would happily anchor on `id=":r5:"` | ||
| * and produce selectors that never match across page loads. | ||
| * | ||
| * 2. Classes — when they're used at all for sibling disambiguation — are | ||
| * filtered through `filterClasses` against the unstable-class pattern | ||
| * pack. The current v1 implementation here doesn't emit classes (we lean | ||
| * entirely on `:nth-of-type` for sibling disambiguation), but the | ||
| * filtering helper is wired through so future iterations can opt back in | ||
| * surgically without re-deriving the policy. | ||
| * | ||
| * 3. A unique anchor id terminates the walk early — once we hit `body#main`, | ||
| * we don't need to keep walking up to `<html>`. | ||
| * | ||
| * 4. Ambiguous id anchors are skipped. Duplicate ids are invalid HTML but | ||
| * common enough in the wild that fallback must not emit selectors that | ||
| * resolve to the wrong target. | ||
| * | ||
| * Output format mirrors the orchestrator's output: `<anchor> > <descent>`. | ||
| * When no usable id is found anywhere on the walk, the fallback emits a | ||
| * pure-positional selector rooted at `html`. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.fallbackCssPath = void 0; | ||
| var tslib_1 = require("tslib"); | ||
| var get_stable_id_1 = require("./helpers/get-stable-id"); | ||
| var escape_id_1 = require("./helpers/escape-id"); | ||
| /** | ||
| * Build a positional CSS selector for `el`, using stable ids as anchors when | ||
| * available and falling back to `tag:nth-of-type(n)` for disambiguation. | ||
| * | ||
| * Honors `config.maxAncestorWalkDepth` defensively — once the depth limit is | ||
| * reached, the walker stops and returns whatever it has built so far rooted at | ||
| * the deepest reached ancestor. | ||
| */ | ||
| function fallbackCssPath(el, config, options) { | ||
| var _a, _b; | ||
| if (options === void 0) { options = {}; } | ||
| var scope = (_b = (_a = options.scope) !== null && _a !== void 0 ? _a : el.ownerDocument) !== null && _b !== void 0 ? _b : document; | ||
| var segments = []; | ||
| var cursor = el; | ||
| var depth = 0; | ||
| while (cursor !== null) { | ||
| if (config.maxAncestorWalkDepth !== undefined && depth > config.maxAncestorWalkDepth) { | ||
| break; | ||
| } | ||
| var id = (0, get_stable_id_1.getStableId)(cursor, config); | ||
| if (id !== null) { | ||
| // Anchor format mirrors the stableId strategy: `tag#id` with the id | ||
| // CSS-escaped. Only terminate if the composed selector uniquely resolves | ||
| // to the target; duplicate ids should fall through to positional steps. | ||
| var anchorSegment = "".concat(cursor.tagName.toLowerCase(), "#").concat((0, escape_id_1.escapeIdForCss)(id)); | ||
| var candidate = tslib_1.__spreadArray([anchorSegment], tslib_1.__read(segments), false).join(' > '); | ||
| if (isUniqueMatch(scope, candidate, el)) { | ||
| return candidate; | ||
| } | ||
| } | ||
| segments.unshift(stepFor(cursor)); | ||
| cursor = cursor.parentElement; | ||
| depth += 1; | ||
| } | ||
| // Walked all the way to the document root without finding an id. The first | ||
| // segment is whatever `<html>` produced (which is just `html` since it has | ||
| // no parent and no same-tag siblings). | ||
| return segments.join(' > '); | ||
| } | ||
| exports.fallbackCssPath = fallbackCssPath; | ||
| /** | ||
| * Build a single step. For an element with a parent: `tag:nth-of-type(n)`. | ||
| * For a root element (no parent — i.e. <html>): just `tag`. | ||
| * | ||
| * Class-based disambiguation is intentionally not emitted in v1 — see the | ||
| * design doc, "Why we don't use classes for sibling disambiguation". The | ||
| * `filterClasses` helper is imported in the package for future iterations. | ||
| */ | ||
| function stepFor(el) { | ||
| var tag = el.tagName.toLowerCase(); | ||
| var parent = el.parentElement; | ||
| if (parent === null) { | ||
| return tag; | ||
| } | ||
| var index = sameTypeIndex(el, parent); | ||
| return "".concat(tag, ":nth-of-type(").concat(index, ")"); | ||
| } | ||
| function sameTypeIndex(el, parent) { | ||
| var count = 0; | ||
| for (var i = 0; i < parent.children.length; i++) { | ||
| var sibling = parent.children[i]; | ||
| if (sibling.tagName === el.tagName) { | ||
| count += 1; | ||
| if (sibling === el) | ||
| return count; | ||
| } | ||
| } | ||
| return 1; | ||
| } | ||
| function isUniqueMatch(scope, selector, el) { | ||
| var matches; | ||
| try { | ||
| matches = scope.querySelectorAll(selector); | ||
| } | ||
| catch (_e) { | ||
| return false; | ||
| } | ||
| return matches.length === 1 && matches[0] === el; | ||
| } | ||
| //# sourceMappingURL=fallback-css-path.js.map |
| {"version":3,"file":"fallback-css-path.js","sourceRoot":"","sources":["../../src/fallback-css-path.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;;;;AAGH,yDAAsD;AACtD,iDAAqD;AAOrD;;;;;;;GAOG;AACH,SAAgB,eAAe,CAC7B,EAAW,EACX,MAA8B,EAC9B,OAAoC;;IAApC,wBAAA,EAAA,YAAoC;IAEpC,IAAM,KAAK,GAAe,MAAA,MAAA,OAAO,CAAC,KAAK,mCAAI,EAAE,CAAC,aAAa,mCAAI,QAAQ,CAAC;IACxE,IAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,MAAM,GAAmB,EAAE,CAAC;IAChC,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,MAAM,KAAK,IAAI,EAAE;QACtB,IAAI,MAAM,CAAC,oBAAoB,KAAK,SAAS,IAAI,KAAK,GAAG,MAAM,CAAC,oBAAoB,EAAE;YACpF,MAAM;SACP;QAED,IAAM,EAAE,GAAG,IAAA,2BAAW,EAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACvC,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,oEAAoE;YACpE,yEAAyE;YACzE,wEAAwE;YACxE,IAAM,aAAa,GAAG,UAAG,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,cAAI,IAAA,0BAAc,EAAC,EAAE,CAAC,CAAE,CAAC;YAC9E,IAAM,SAAS,GAAG,uBAAC,aAAa,kBAAK,QAAQ,UAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3D,IAAI,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC,EAAE;gBACvC,OAAO,SAAS,CAAC;aAClB;SACF;QAED,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAClC,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC;QAC9B,KAAK,IAAI,CAAC,CAAC;KACZ;IAED,2EAA2E;IAC3E,2EAA2E;IAC3E,uCAAuC;IACvC,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AApCD,0CAoCC;AAED;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,EAAW;IAC1B,IAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IACrC,IAAM,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC;IAChC,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,OAAO,GAAG,CAAC;KACZ;IACD,IAAM,KAAK,GAAG,aAAa,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,UAAG,GAAG,0BAAgB,KAAK,MAAG,CAAC;AACxC,CAAC;AAED,SAAS,aAAa,CAAC,EAAW,EAAE,MAAe;IACjD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QAC/C,IAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,OAAO,CAAC,OAAO,KAAK,EAAE,CAAC,OAAO,EAAE;YAClC,KAAK,IAAI,CAAC,CAAC;YACX,IAAI,OAAO,KAAK,EAAE;gBAAE,OAAO,KAAK,CAAC;SAClC;KACF;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,aAAa,CAAC,KAAiB,EAAE,QAAgB,EAAE,EAAW;IACrE,IAAI,OAA4B,CAAC;IACjC,IAAI;QACF,OAAO,GAAG,KAAK,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;KAC5C;IAAC,OAAO,EAAE,EAAE;QACX,OAAO,KAAK,CAAC;KACd;IACD,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;AACnD,CAAC","sourcesContent":["/**\n * Fallback CSS-path walker — the safety net for the v1 element-selector\n * algorithm.\n *\n * When the orchestrator's strategy chain finds nothing usable across the full\n * ancestor walk, the engine invokes this fallback. It produces a positional\n * selector by walking up from the target to <html>, expressing each step as\n * either an id anchor (when one survives the autogen filter) or a\n * `tag:nth-of-type(n)` step.\n *\n * Key differences from the legacy autocapture `cssPath`:\n *\n * 1. Ids are filtered through `getStableId`, which honors the\n * explicit-tracking-attribute suppression signal AND the autogenerated-id\n * pattern pack. The legacy `cssPath` would happily anchor on `id=\":r5:\"`\n * and produce selectors that never match across page loads.\n *\n * 2. Classes — when they're used at all for sibling disambiguation — are\n * filtered through `filterClasses` against the unstable-class pattern\n * pack. The current v1 implementation here doesn't emit classes (we lean\n * entirely on `:nth-of-type` for sibling disambiguation), but the\n * filtering helper is wired through so future iterations can opt back in\n * surgically without re-deriving the policy.\n *\n * 3. A unique anchor id terminates the walk early — once we hit `body#main`,\n * we don't need to keep walking up to `<html>`.\n *\n * 4. Ambiguous id anchors are skipped. Duplicate ids are invalid HTML but\n * common enough in the wild that fallback must not emit selectors that\n * resolve to the wrong target.\n *\n * Output format mirrors the orchestrator's output: `<anchor> > <descent>`.\n * When no usable id is found anywhere on the walk, the fallback emits a\n * pure-positional selector rooted at `html`.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ResolvedSelectorConfig } from './types';\nimport { getStableId } from './helpers/get-stable-id';\nimport { escapeIdForCss } from './helpers/escape-id';\n\nexport interface FallbackCssPathOptions {\n /** Document or shadow root used for uniqueness checks. Defaults to the target's owner document. */\n scope?: ParentNode;\n}\n\n/**\n * Build a positional CSS selector for `el`, using stable ids as anchors when\n * available and falling back to `tag:nth-of-type(n)` for disambiguation.\n *\n * Honors `config.maxAncestorWalkDepth` defensively — once the depth limit is\n * reached, the walker stops and returns whatever it has built so far rooted at\n * the deepest reached ancestor.\n */\nexport function fallbackCssPath(\n el: Element,\n config: ResolvedSelectorConfig,\n options: FallbackCssPathOptions = {},\n): string {\n const scope: ParentNode = options.scope ?? el.ownerDocument ?? document;\n const segments: string[] = [];\n let cursor: Element | null = el;\n let depth = 0;\n\n while (cursor !== null) {\n if (config.maxAncestorWalkDepth !== undefined && depth > config.maxAncestorWalkDepth) {\n break;\n }\n\n const id = getStableId(cursor, config);\n if (id !== null) {\n // Anchor format mirrors the stableId strategy: `tag#id` with the id\n // CSS-escaped. Only terminate if the composed selector uniquely resolves\n // to the target; duplicate ids should fall through to positional steps.\n const anchorSegment = `${cursor.tagName.toLowerCase()}#${escapeIdForCss(id)}`;\n const candidate = [anchorSegment, ...segments].join(' > ');\n if (isUniqueMatch(scope, candidate, el)) {\n return candidate;\n }\n }\n\n segments.unshift(stepFor(cursor));\n cursor = cursor.parentElement;\n depth += 1;\n }\n\n // Walked all the way to the document root without finding an id. The first\n // segment is whatever `<html>` produced (which is just `html` since it has\n // no parent and no same-tag siblings).\n return segments.join(' > ');\n}\n\n/**\n * Build a single step. For an element with a parent: `tag:nth-of-type(n)`.\n * For a root element (no parent — i.e. <html>): just `tag`.\n *\n * Class-based disambiguation is intentionally not emitted in v1 — see the\n * design doc, \"Why we don't use classes for sibling disambiguation\". The\n * `filterClasses` helper is imported in the package for future iterations.\n */\nfunction stepFor(el: Element): string {\n const tag = el.tagName.toLowerCase();\n const parent = el.parentElement;\n if (parent === null) {\n return tag;\n }\n const index = sameTypeIndex(el, parent);\n return `${tag}:nth-of-type(${index})`;\n}\n\nfunction sameTypeIndex(el: Element, parent: Element): number {\n let count = 0;\n for (let i = 0; i < parent.children.length; i++) {\n const sibling = parent.children[i];\n if (sibling.tagName === el.tagName) {\n count += 1;\n if (sibling === el) return count;\n }\n }\n return 1;\n}\n\nfunction isUniqueMatch(scope: ParentNode, selector: string, el: Element): boolean {\n let matches: NodeListOf<Element>;\n try {\n matches = scope.querySelectorAll(selector);\n } catch (_e) {\n return false;\n }\n return matches.length === 1 && matches[0] === el;\n}\n"]} |
| /** | ||
| * Positional descent builder. | ||
| * | ||
| * Given an anchor element (the result of pass 2's walk in the orchestration PR) | ||
| * and the trail of intermediate elements between the anchor and the original | ||
| * click target, produce the CSS-selector descent string. Each step in the | ||
| * descent uses `tag:nth-of-type(n)` so the resulting selector resolves | ||
| * unambiguously to the original target regardless of class-state churn. | ||
| * | ||
| * Matches ContentSquare's "position within identical markers" convention. | ||
| * | ||
| * Example: | ||
| * | ||
| * describeRelative(anchor, [<section>, <ul>, <li>]) | ||
| * → "section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)" | ||
| * | ||
| * Combined with the anchor selector (`anchor#some-id`) by the orchestrator: | ||
| * | ||
| * "anchor#some-id > section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)" | ||
| * | ||
| * The `anchor` parameter is accepted for symmetry with the orchestrator's call | ||
| * site but isn't currently used — the function only needs each trail element's | ||
| * own parent context. We keep the signature so subsequent work can reference | ||
| * the anchor when extending the descent format (e.g., to optimize bare | ||
| * `:nth-of-type(1)` away when there's only one of that type). | ||
| */ | ||
| export declare function describeRelative(_anchor: Element, trail: Element[]): string; | ||
| //# sourceMappingURL=describe-relative.d.ts.map |
| {"version":3,"file":"describe-relative.d.ts","sourceRoot":"","sources":["../../../src/helpers/describe-relative.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAE3E"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.describeRelative = void 0; | ||
| /** | ||
| * Positional descent builder. | ||
| * | ||
| * Given an anchor element (the result of pass 2's walk in the orchestration PR) | ||
| * and the trail of intermediate elements between the anchor and the original | ||
| * click target, produce the CSS-selector descent string. Each step in the | ||
| * descent uses `tag:nth-of-type(n)` so the resulting selector resolves | ||
| * unambiguously to the original target regardless of class-state churn. | ||
| * | ||
| * Matches ContentSquare's "position within identical markers" convention. | ||
| * | ||
| * Example: | ||
| * | ||
| * describeRelative(anchor, [<section>, <ul>, <li>]) | ||
| * → "section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)" | ||
| * | ||
| * Combined with the anchor selector (`anchor#some-id`) by the orchestrator: | ||
| * | ||
| * "anchor#some-id > section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)" | ||
| * | ||
| * The `anchor` parameter is accepted for symmetry with the orchestrator's call | ||
| * site but isn't currently used — the function only needs each trail element's | ||
| * own parent context. We keep the signature so subsequent work can reference | ||
| * the anchor when extending the descent format (e.g., to optimize bare | ||
| * `:nth-of-type(1)` away when there's only one of that type). | ||
| */ | ||
| function describeRelative(_anchor, trail) { | ||
| return trail.map(stepFor).join(' > '); | ||
| } | ||
| exports.describeRelative = describeRelative; | ||
| function stepFor(el) { | ||
| var tag = el.tagName.toLowerCase(); | ||
| var parent = el.parentElement; | ||
| if (parent === null) { | ||
| // Detached element or root — emit just the tag. The orchestrator shouldn't | ||
| // call us with a detached trail, but we don't want to crash if it does. | ||
| return tag; | ||
| } | ||
| var index = sameTypeIndex(el, parent); | ||
| return "".concat(tag, ":nth-of-type(").concat(index, ")"); | ||
| } | ||
| function sameTypeIndex(el, parent) { | ||
| // 1-based index among same-tag element siblings, mirroring :nth-of-type semantics. | ||
| var count = 0; | ||
| for (var i = 0; i < parent.children.length; i++) { | ||
| var sibling = parent.children[i]; | ||
| if (sibling.tagName === el.tagName) { | ||
| count += 1; | ||
| if (sibling === el) | ||
| return count; | ||
| } | ||
| } | ||
| // Element wasn't found among its parent's children — shouldn't happen for a | ||
| // live element. Return 1 as a defensive fallback rather than throwing. | ||
| return 1; | ||
| } | ||
| //# sourceMappingURL=describe-relative.js.map |
| {"version":3,"file":"describe-relative.js","sourceRoot":"","sources":["../../../src/helpers/describe-relative.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,SAAgB,gBAAgB,CAAC,OAAgB,EAAE,KAAgB;IACjE,OAAO,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAFD,4CAEC;AAED,SAAS,OAAO,CAAC,EAAW;IAC1B,IAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IACrC,IAAM,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC;IAChC,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,2EAA2E;QAC3E,wEAAwE;QACxE,OAAO,GAAG,CAAC;KACZ;IACD,IAAM,KAAK,GAAG,aAAa,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,UAAG,GAAG,0BAAgB,KAAK,MAAG,CAAC;AACxC,CAAC;AAED,SAAS,aAAa,CAAC,EAAW,EAAE,MAAe;IACjD,mFAAmF;IACnF,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QAC/C,IAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,OAAO,CAAC,OAAO,KAAK,EAAE,CAAC,OAAO,EAAE;YAClC,KAAK,IAAI,CAAC,CAAC;YACX,IAAI,OAAO,KAAK,EAAE;gBAAE,OAAO,KAAK,CAAC;SAClC;KACF;IACD,4EAA4E;IAC5E,uEAAuE;IACvE,OAAO,CAAC,CAAC;AACX,CAAC","sourcesContent":["/**\n * Positional descent builder.\n *\n * Given an anchor element (the result of pass 2's walk in the orchestration PR)\n * and the trail of intermediate elements between the anchor and the original\n * click target, produce the CSS-selector descent string. Each step in the\n * descent uses `tag:nth-of-type(n)` so the resulting selector resolves\n * unambiguously to the original target regardless of class-state churn.\n *\n * Matches ContentSquare's \"position within identical markers\" convention.\n *\n * Example:\n *\n * describeRelative(anchor, [<section>, <ul>, <li>])\n * → \"section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)\"\n *\n * Combined with the anchor selector (`anchor#some-id`) by the orchestrator:\n *\n * \"anchor#some-id > section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)\"\n *\n * The `anchor` parameter is accepted for symmetry with the orchestrator's call\n * site but isn't currently used — the function only needs each trail element's\n * own parent context. We keep the signature so subsequent work can reference\n * the anchor when extending the descent format (e.g., to optimize bare\n * `:nth-of-type(1)` away when there's only one of that type).\n */\nexport function describeRelative(_anchor: Element, trail: Element[]): string {\n return trail.map(stepFor).join(' > ');\n}\n\nfunction stepFor(el: Element): string {\n const tag = el.tagName.toLowerCase();\n const parent = el.parentElement;\n if (parent === null) {\n // Detached element or root — emit just the tag. The orchestrator shouldn't\n // call us with a detached trail, but we don't want to crash if it does.\n return tag;\n }\n const index = sameTypeIndex(el, parent);\n return `${tag}:nth-of-type(${index})`;\n}\n\nfunction sameTypeIndex(el: Element, parent: Element): number {\n // 1-based index among same-tag element siblings, mirroring :nth-of-type semantics.\n let count = 0;\n for (let i = 0; i < parent.children.length; i++) {\n const sibling = parent.children[i];\n if (sibling.tagName === el.tagName) {\n count += 1;\n if (sibling === el) return count;\n }\n }\n // Element wasn't found among its parent's children — shouldn't happen for a\n // live element. Return 1 as a defensive fallback rather than throwing.\n return 1;\n}\n"]} |
| /** | ||
| * Escape a string for use as a CSS identifier. | ||
| * | ||
| * Uses the native CSS.escape implementation when available, with the CSSOM | ||
| * algorithm inlined for runtimes that do not expose it (notably jsdom). | ||
| */ | ||
| export declare function escapeCssIdentifier(value: string): string; | ||
| //# sourceMappingURL=escape-css-identifier.d.ts.map |
| {"version":3,"file":"escape-css-identifier.d.ts","sourceRoot":"","sources":["../../../src/helpers/escape-css-identifier.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiDzD"} |
| "use strict"; | ||
| /* eslint-disable no-restricted-globals */ | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.escapeCssIdentifier = void 0; | ||
| /** | ||
| * Escape a string for use as a CSS identifier. | ||
| * | ||
| * Uses the native CSS.escape implementation when available, with the CSSOM | ||
| * algorithm inlined for runtimes that do not expose it (notably jsdom). | ||
| */ | ||
| function escapeCssIdentifier(value) { | ||
| var css = globalThis.CSS; | ||
| if (css && typeof css.escape === 'function') { | ||
| return css.escape(value); | ||
| } | ||
| var string = String(value); | ||
| var length = string.length; | ||
| var result = ''; | ||
| for (var index = 0; index < length; index++) { | ||
| var codeUnit = string.charCodeAt(index); | ||
| if (codeUnit === 0x0000) { | ||
| result += '\uFFFD'; | ||
| continue; | ||
| } | ||
| if ((codeUnit >= 0x0001 && codeUnit <= 0x001f) || | ||
| codeUnit === 0x007f || | ||
| (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || | ||
| (index === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && string.charCodeAt(0) === 0x002d)) { | ||
| result += "\\".concat(codeUnit.toString(16), " "); | ||
| continue; | ||
| } | ||
| if (index === 0 && length === 1 && codeUnit === 0x002d) { | ||
| result += '\\-'; | ||
| continue; | ||
| } | ||
| if (codeUnit >= 0x0080 || | ||
| codeUnit === 0x002d || | ||
| codeUnit === 0x005f || | ||
| (codeUnit >= 0x0030 && codeUnit <= 0x0039) || | ||
| (codeUnit >= 0x0041 && codeUnit <= 0x005a) || | ||
| (codeUnit >= 0x0061 && codeUnit <= 0x007a)) { | ||
| result += string.charAt(index); | ||
| continue; | ||
| } | ||
| result += "\\".concat(string.charAt(index)); | ||
| } | ||
| return result; | ||
| } | ||
| exports.escapeCssIdentifier = escapeCssIdentifier; | ||
| //# sourceMappingURL=escape-css-identifier.js.map |
| {"version":3,"file":"escape-css-identifier.js","sourceRoot":"","sources":["../../../src/helpers/escape-css-identifier.ts"],"names":[],"mappings":";AAAA,0CAA0C;;;AAQ1C;;;;;GAKG;AACH,SAAgB,mBAAmB,CAAC,KAAa;IAC/C,IAAM,GAAG,GAAI,UAA8B,CAAC,GAAG,CAAC;IAChD,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,UAAU,EAAE;QAC3C,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;KAC1B;IAED,IAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE;QAC3C,IAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAE1C,IAAI,QAAQ,KAAK,MAAM,EAAE;YACvB,MAAM,IAAI,QAAQ,CAAC;YACnB,SAAS;SACV;QAED,IACE,CAAC,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC;YAC1C,QAAQ,KAAK,MAAM;YACnB,CAAC,KAAK,KAAK,CAAC,IAAI,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC;YACzD,CAAC,KAAK,KAAK,CAAC,IAAI,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,EAC5F;YACA,MAAM,IAAI,YAAK,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAG,CAAC;YACxC,SAAS;SACV;QAED,IAAI,KAAK,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,IAAI,QAAQ,KAAK,MAAM,EAAE;YACtD,MAAM,IAAI,KAAK,CAAC;YAChB,SAAS;SACV;QAED,IACE,QAAQ,IAAI,MAAM;YAClB,QAAQ,KAAK,MAAM;YACnB,QAAQ,KAAK,MAAM;YACnB,CAAC,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC;YAC1C,CAAC,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC;YAC1C,CAAC,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC,EAC1C;YACA,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC/B,SAAS;SACV;QAED,MAAM,IAAI,YAAK,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAE,CAAC;KACvC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC;AAjDD,kDAiDC","sourcesContent":["/* eslint-disable no-restricted-globals */\n\ntype CssEscapeGlobal = {\n CSS?: {\n escape?: (value: string) => string;\n };\n};\n\n/**\n * Escape a string for use as a CSS identifier.\n *\n * Uses the native CSS.escape implementation when available, with the CSSOM\n * algorithm inlined for runtimes that do not expose it (notably jsdom).\n */\nexport function escapeCssIdentifier(value: string): string {\n const css = (globalThis as CssEscapeGlobal).CSS;\n if (css && typeof css.escape === 'function') {\n return css.escape(value);\n }\n\n const string = String(value);\n const length = string.length;\n let result = '';\n\n for (let index = 0; index < length; index++) {\n const codeUnit = string.charCodeAt(index);\n\n if (codeUnit === 0x0000) {\n result += '\\uFFFD';\n continue;\n }\n\n if (\n (codeUnit >= 0x0001 && codeUnit <= 0x001f) ||\n codeUnit === 0x007f ||\n (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||\n (index === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && string.charCodeAt(0) === 0x002d)\n ) {\n result += `\\\\${codeUnit.toString(16)} `;\n continue;\n }\n\n if (index === 0 && length === 1 && codeUnit === 0x002d) {\n result += '\\\\-';\n continue;\n }\n\n if (\n codeUnit >= 0x0080 ||\n codeUnit === 0x002d ||\n codeUnit === 0x005f ||\n (codeUnit >= 0x0030 && codeUnit <= 0x0039) ||\n (codeUnit >= 0x0041 && codeUnit <= 0x005a) ||\n (codeUnit >= 0x0061 && codeUnit <= 0x007a)\n ) {\n result += string.charAt(index);\n continue;\n }\n\n result += `\\\\${string.charAt(index)}`;\n }\n\n return result;\n}\n"]} |
| /** Escape an element id for the `tag#<id>` selector form shared by strategies and fallback. */ | ||
| export declare function escapeIdForCss(id: string): string; | ||
| //# sourceMappingURL=escape-id.d.ts.map |
| {"version":3,"file":"escape-id.d.ts","sourceRoot":"","sources":["../../../src/helpers/escape-id.ts"],"names":[],"mappings":"AAEA,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.escapeIdForCss = void 0; | ||
| var escape_css_identifier_1 = require("./escape-css-identifier"); | ||
| /** Escape an element id for the `tag#<id>` selector form shared by strategies and fallback. */ | ||
| function escapeIdForCss(id) { | ||
| return (0, escape_css_identifier_1.escapeCssIdentifier)(id); | ||
| } | ||
| exports.escapeIdForCss = escapeIdForCss; | ||
| //# sourceMappingURL=escape-id.js.map |
| {"version":3,"file":"escape-id.js","sourceRoot":"","sources":["../../../src/helpers/escape-id.ts"],"names":[],"mappings":";;;AAAA,iEAA8D;AAE9D,+FAA+F;AAC/F,SAAgB,cAAc,CAAC,EAAU;IACvC,OAAO,IAAA,2CAAmB,EAAC,EAAE,CAAC,CAAC;AACjC,CAAC;AAFD,wCAEC","sourcesContent":["import { escapeCssIdentifier } from './escape-css-identifier';\n\n/** Escape an element id for the `tag#<id>` selector form shared by strategies and fallback. */\nexport function escapeIdForCss(id: string): string {\n return escapeCssIdentifier(id);\n}\n"]} |
| import { ResolvedSelectorConfig } from '../types'; | ||
| /** | ||
| * Shared helper that the `stableId` strategy (in this PR) and the | ||
| * `fallback-css-path` walker (in the orchestration PR) both consult to decide | ||
| * whether an element's id is usable as a selector anchor. | ||
| * | ||
| * Resolution order: | ||
| * | ||
| * 1. Check the customer's explicit-tracking attribute. If set with an empty | ||
| * value (e.g. `data-amp-track-id=""`), that's an explicit suppression | ||
| * signal — the customer is telling us to ignore this element's id even | ||
| * if it would otherwise look stable. Return null. | ||
| * 2. Read the element's id. If absent or empty, return null. | ||
| * 3. Check the id against the autogenerated-id pattern pack. If matched, | ||
| * return null (the algorithm should walk past this element). | ||
| * 4. Otherwise return the id. | ||
| * | ||
| * The single point of consultation means the strategy and the fallback always | ||
| * agree on what's a usable id — no chance of one component filtering an id | ||
| * while another uses it anyway. | ||
| */ | ||
| export declare function getStableId(el: Element, cfg: ResolvedSelectorConfig): string | null; | ||
| //# sourceMappingURL=get-stable-id.d.ts.map |
| {"version":3,"file":"get-stable-id.d.ts","sourceRoot":"","sources":["../../../src/helpers/get-stable-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAGlD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,sBAAsB,GAAG,MAAM,GAAG,IAAI,CAoBnF"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.getStableId = void 0; | ||
| var autogenerated_ids_1 = require("../patterns/autogenerated-ids"); | ||
| /** | ||
| * Shared helper that the `stableId` strategy (in this PR) and the | ||
| * `fallback-css-path` walker (in the orchestration PR) both consult to decide | ||
| * whether an element's id is usable as a selector anchor. | ||
| * | ||
| * Resolution order: | ||
| * | ||
| * 1. Check the customer's explicit-tracking attribute. If set with an empty | ||
| * value (e.g. `data-amp-track-id=""`), that's an explicit suppression | ||
| * signal — the customer is telling us to ignore this element's id even | ||
| * if it would otherwise look stable. Return null. | ||
| * 2. Read the element's id. If absent or empty, return null. | ||
| * 3. Check the id against the autogenerated-id pattern pack. If matched, | ||
| * return null (the algorithm should walk past this element). | ||
| * 4. Otherwise return the id. | ||
| * | ||
| * The single point of consultation means the strategy and the fallback always | ||
| * agree on what's a usable id — no chance of one component filtering an id | ||
| * while another uses it anyway. | ||
| */ | ||
| function getStableId(el, cfg) { | ||
| // 1. Suppression signal: empty explicit-tracking attribute on this element. | ||
| var trackAttr = el.getAttribute(cfg.explicitTrackingAttribute); | ||
| if (trackAttr !== null && trackAttr === '') { | ||
| return null; | ||
| } | ||
| // 2. Id presence. | ||
| var id = el.getAttribute('id'); | ||
| if (id === null || id === '') { | ||
| return null; | ||
| } | ||
| // 3. Autogenerated-pattern filter. | ||
| if (!(0, autogenerated_ids_1.isStableId)(id, cfg.autogeneratedIdPatterns)) { | ||
| return null; | ||
| } | ||
| // 4. Stable. | ||
| return id; | ||
| } | ||
| exports.getStableId = getStableId; | ||
| //# sourceMappingURL=get-stable-id.js.map |
| {"version":3,"file":"get-stable-id.js","sourceRoot":"","sources":["../../../src/helpers/get-stable-id.ts"],"names":[],"mappings":";;;AACA,mEAA2D;AAE3D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,SAAgB,WAAW,CAAC,EAAW,EAAE,GAA2B;IAClE,4EAA4E;IAC5E,IAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACjE,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,EAAE,EAAE;QAC1C,OAAO,IAAI,CAAC;KACb;IAED,kBAAkB;IAClB,IAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,EAAE,EAAE;QAC5B,OAAO,IAAI,CAAC;KACb;IAED,mCAAmC;IACnC,IAAI,CAAC,IAAA,8BAAU,EAAC,EAAE,EAAE,GAAG,CAAC,uBAAuB,CAAC,EAAE;QAChD,OAAO,IAAI,CAAC;KACb;IAED,aAAa;IACb,OAAO,EAAE,CAAC;AACZ,CAAC;AApBD,kCAoBC","sourcesContent":["import { ResolvedSelectorConfig } from '../types';\nimport { isStableId } from '../patterns/autogenerated-ids';\n\n/**\n * Shared helper that the `stableId` strategy (in this PR) and the\n * `fallback-css-path` walker (in the orchestration PR) both consult to decide\n * whether an element's id is usable as a selector anchor.\n *\n * Resolution order:\n *\n * 1. Check the customer's explicit-tracking attribute. If set with an empty\n * value (e.g. `data-amp-track-id=\"\"`), that's an explicit suppression\n * signal — the customer is telling us to ignore this element's id even\n * if it would otherwise look stable. Return null.\n * 2. Read the element's id. If absent or empty, return null.\n * 3. Check the id against the autogenerated-id pattern pack. If matched,\n * return null (the algorithm should walk past this element).\n * 4. Otherwise return the id.\n *\n * The single point of consultation means the strategy and the fallback always\n * agree on what's a usable id — no chance of one component filtering an id\n * while another uses it anyway.\n */\nexport function getStableId(el: Element, cfg: ResolvedSelectorConfig): string | null {\n // 1. Suppression signal: empty explicit-tracking attribute on this element.\n const trackAttr = el.getAttribute(cfg.explicitTrackingAttribute);\n if (trackAttr !== null && trackAttr === '') {\n return null;\n }\n\n // 2. Id presence.\n const id = el.getAttribute('id');\n if (id === null || id === '') {\n return null;\n }\n\n // 3. Autogenerated-pattern filter.\n if (!isStableId(id, cfg.autogeneratedIdPatterns)) {\n return null;\n }\n\n // 4. Stable.\n return id;\n}\n"]} |
| /** | ||
| * @amplitude/element-selector | ||
| * | ||
| * Shared element-selector algorithm consumed by autocapture, the | ||
| * app.amplitude.com tagging UI, and the Chrome extension visual tagger. | ||
| * | ||
| * This file is the package's public API surface. The orchestrator, fallback, | ||
| * config resolver, and engine factory compose on top of the primitives | ||
| * (types, pattern packs, helpers, strategies) to produce the runtime engine | ||
| * consumers actually call. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| export declare const PACKAGE_NAME = "@amplitude/element-selector"; | ||
| export type { Strategy, StrategyContext, ResolvedSelectorConfig, ElementSelectorRemoteConfig, ElementSelectorLogger, SelectorEngine, } from './types'; | ||
| export { DEFAULT_AUTOGENERATED_ID_PATTERNS, compile as compileAutogeneratedIdPatterns, isStableId, } from './patterns/autogenerated-ids'; | ||
| export { DEFAULT_UNSTABLE_CLASS_PATTERNS, compile as compileUnstableClassPatterns, filterClasses, } from './patterns/unstable-classes'; | ||
| export { getStableId } from './helpers/get-stable-id'; | ||
| export { describeRelative } from './helpers/describe-relative'; | ||
| export { explicitTrackingAttribute } from './strategies/explicit-tracking-attribute'; | ||
| export { stableId as stableIdStrategy } from './strategies/stable-id'; | ||
| export { runOrchestrator, DEFAULT_STRATEGIES } from './orchestrator'; | ||
| export type { OrchestratorOptions } from './orchestrator'; | ||
| export { fallbackCssPath } from './fallback-css-path'; | ||
| export { resolveSelectorConfig, DEFAULT_RESOLVED_CONFIG } from './config/resolve-config'; | ||
| export { createSelectorEngine } from './engine'; | ||
| export type { CreateSelectorEngineOptions } from './engine'; | ||
| //# sourceMappingURL=index.d.ts.map |
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,YAAY,gCAAgC,CAAC;AAG1D,YAAY,EACV,QAAQ,EACR,eAAe,EACf,sBAAsB,EACtB,2BAA2B,EAC3B,qBAAqB,EACrB,cAAc,GACf,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,iCAAiC,EACjC,OAAO,IAAI,8BAA8B,EACzC,UAAU,GACX,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,+BAA+B,EAC/B,OAAO,IAAI,4BAA4B,EACvC,aAAa,GACd,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,OAAO,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAKtE,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACrE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAGzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC"} |
| "use strict"; | ||
| /** | ||
| * @amplitude/element-selector | ||
| * | ||
| * Shared element-selector algorithm consumed by autocapture, the | ||
| * app.amplitude.com tagging UI, and the Chrome extension visual tagger. | ||
| * | ||
| * This file is the package's public API surface. The orchestrator, fallback, | ||
| * config resolver, and engine factory compose on top of the primitives | ||
| * (types, pattern packs, helpers, strategies) to produce the runtime engine | ||
| * consumers actually call. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.createSelectorEngine = exports.DEFAULT_RESOLVED_CONFIG = exports.resolveSelectorConfig = exports.fallbackCssPath = exports.DEFAULT_STRATEGIES = exports.runOrchestrator = exports.stableIdStrategy = exports.explicitTrackingAttribute = exports.describeRelative = exports.getStableId = exports.filterClasses = exports.compileUnstableClassPatterns = exports.DEFAULT_UNSTABLE_CLASS_PATTERNS = exports.isStableId = exports.compileAutogeneratedIdPatterns = exports.DEFAULT_AUTOGENERATED_ID_PATTERNS = exports.PACKAGE_NAME = void 0; | ||
| exports.PACKAGE_NAME = '@amplitude/element-selector'; | ||
| // ===== Pattern packs ===== | ||
| var autogenerated_ids_1 = require("./patterns/autogenerated-ids"); | ||
| Object.defineProperty(exports, "DEFAULT_AUTOGENERATED_ID_PATTERNS", { enumerable: true, get: function () { return autogenerated_ids_1.DEFAULT_AUTOGENERATED_ID_PATTERNS; } }); | ||
| Object.defineProperty(exports, "compileAutogeneratedIdPatterns", { enumerable: true, get: function () { return autogenerated_ids_1.compile; } }); | ||
| Object.defineProperty(exports, "isStableId", { enumerable: true, get: function () { return autogenerated_ids_1.isStableId; } }); | ||
| var unstable_classes_1 = require("./patterns/unstable-classes"); | ||
| Object.defineProperty(exports, "DEFAULT_UNSTABLE_CLASS_PATTERNS", { enumerable: true, get: function () { return unstable_classes_1.DEFAULT_UNSTABLE_CLASS_PATTERNS; } }); | ||
| Object.defineProperty(exports, "compileUnstableClassPatterns", { enumerable: true, get: function () { return unstable_classes_1.compile; } }); | ||
| Object.defineProperty(exports, "filterClasses", { enumerable: true, get: function () { return unstable_classes_1.filterClasses; } }); | ||
| // ===== Helpers ===== | ||
| var get_stable_id_1 = require("./helpers/get-stable-id"); | ||
| Object.defineProperty(exports, "getStableId", { enumerable: true, get: function () { return get_stable_id_1.getStableId; } }); | ||
| var describe_relative_1 = require("./helpers/describe-relative"); | ||
| Object.defineProperty(exports, "describeRelative", { enumerable: true, get: function () { return describe_relative_1.describeRelative; } }); | ||
| // ===== Strategies ===== | ||
| var explicit_tracking_attribute_1 = require("./strategies/explicit-tracking-attribute"); | ||
| Object.defineProperty(exports, "explicitTrackingAttribute", { enumerable: true, get: function () { return explicit_tracking_attribute_1.explicitTrackingAttribute; } }); | ||
| var stable_id_1 = require("./strategies/stable-id"); | ||
| Object.defineProperty(exports, "stableIdStrategy", { enumerable: true, get: function () { return stable_id_1.stableId; } }); | ||
| // ===== Orchestrator + fallback ===== | ||
| // Exposed publicly so the dashboard / Chrome extension can call them ad-hoc | ||
| // without instantiating a full engine — useful for preview / debugging flows. | ||
| var orchestrator_1 = require("./orchestrator"); | ||
| Object.defineProperty(exports, "runOrchestrator", { enumerable: true, get: function () { return orchestrator_1.runOrchestrator; } }); | ||
| Object.defineProperty(exports, "DEFAULT_STRATEGIES", { enumerable: true, get: function () { return orchestrator_1.DEFAULT_STRATEGIES; } }); | ||
| var fallback_css_path_1 = require("./fallback-css-path"); | ||
| Object.defineProperty(exports, "fallbackCssPath", { enumerable: true, get: function () { return fallback_css_path_1.fallbackCssPath; } }); | ||
| // ===== Config resolver ===== | ||
| var resolve_config_1 = require("./config/resolve-config"); | ||
| Object.defineProperty(exports, "resolveSelectorConfig", { enumerable: true, get: function () { return resolve_config_1.resolveSelectorConfig; } }); | ||
| Object.defineProperty(exports, "DEFAULT_RESOLVED_CONFIG", { enumerable: true, get: function () { return resolve_config_1.DEFAULT_RESOLVED_CONFIG; } }); | ||
| // ===== Engine factory ===== | ||
| var engine_1 = require("./engine"); | ||
| Object.defineProperty(exports, "createSelectorEngine", { enumerable: true, get: function () { return engine_1.createSelectorEngine; } }); | ||
| //# sourceMappingURL=index.js.map |
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;;AAEU,QAAA,YAAY,GAAG,6BAA6B,CAAC;AAY1D,4BAA4B;AAC5B,kEAIsC;AAHpC,sIAAA,iCAAiC,OAAA;AACjC,mIAAA,OAAO,OAAkC;AACzC,+GAAA,UAAU,OAAA;AAGZ,gEAIqC;AAHnC,mIAAA,+BAA+B,OAAA;AAC/B,gIAAA,OAAO,OAAgC;AACvC,iHAAA,aAAa,OAAA;AAGf,sBAAsB;AACtB,yDAAsD;AAA7C,4GAAA,WAAW,OAAA;AACpB,iEAA+D;AAAtD,qHAAA,gBAAgB,OAAA;AAEzB,yBAAyB;AACzB,wFAAqF;AAA5E,wIAAA,yBAAyB,OAAA;AAClC,oDAAsE;AAA7D,6GAAA,QAAQ,OAAoB;AAErC,sCAAsC;AACtC,4EAA4E;AAC5E,8EAA8E;AAC9E,+CAAqE;AAA5D,+GAAA,eAAe,OAAA;AAAE,kHAAA,kBAAkB,OAAA;AAE5C,yDAAsD;AAA7C,oHAAA,eAAe,OAAA;AAExB,8BAA8B;AAC9B,0DAAyF;AAAhF,uHAAA,qBAAqB,OAAA;AAAE,yHAAA,uBAAuB,OAAA;AAEvD,6BAA6B;AAC7B,mCAAgD;AAAvC,8GAAA,oBAAoB,OAAA","sourcesContent":["/**\n * @amplitude/element-selector\n *\n * Shared element-selector algorithm consumed by autocapture, the\n * app.amplitude.com tagging UI, and the Chrome extension visual tagger.\n *\n * This file is the package's public API surface. The orchestrator, fallback,\n * config resolver, and engine factory compose on top of the primitives\n * (types, pattern packs, helpers, strategies) to produce the runtime engine\n * consumers actually call.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nexport const PACKAGE_NAME = '@amplitude/element-selector';\n\n// ===== Types =====\nexport type {\n Strategy,\n StrategyContext,\n ResolvedSelectorConfig,\n ElementSelectorRemoteConfig,\n ElementSelectorLogger,\n SelectorEngine,\n} from './types';\n\n// ===== Pattern packs =====\nexport {\n DEFAULT_AUTOGENERATED_ID_PATTERNS,\n compile as compileAutogeneratedIdPatterns,\n isStableId,\n} from './patterns/autogenerated-ids';\n\nexport {\n DEFAULT_UNSTABLE_CLASS_PATTERNS,\n compile as compileUnstableClassPatterns,\n filterClasses,\n} from './patterns/unstable-classes';\n\n// ===== Helpers =====\nexport { getStableId } from './helpers/get-stable-id';\nexport { describeRelative } from './helpers/describe-relative';\n\n// ===== Strategies =====\nexport { explicitTrackingAttribute } from './strategies/explicit-tracking-attribute';\nexport { stableId as stableIdStrategy } from './strategies/stable-id';\n\n// ===== Orchestrator + fallback =====\n// Exposed publicly so the dashboard / Chrome extension can call them ad-hoc\n// without instantiating a full engine — useful for preview / debugging flows.\nexport { runOrchestrator, DEFAULT_STRATEGIES } from './orchestrator';\nexport type { OrchestratorOptions } from './orchestrator';\nexport { fallbackCssPath } from './fallback-css-path';\n\n// ===== Config resolver =====\nexport { resolveSelectorConfig, DEFAULT_RESOLVED_CONFIG } from './config/resolve-config';\n\n// ===== Engine factory =====\nexport { createSelectorEngine } from './engine';\nexport type { CreateSelectorEngineOptions } from './engine';\n"]} |
| /** | ||
| * Orchestrator — the entry point for the v1 element-selector algorithm. | ||
| * | ||
| * Given a click target, the orchestrator walks from the target up through its | ||
| * ancestors and tries each registered strategy at each level. The first | ||
| * strategy that returns a non-null candidate (and produces a selector that | ||
| * resolves uniquely under the scope) wins; the trail of intermediate elements | ||
| * between the anchor and the original target is then expressed as a positional | ||
| * descent via `describeRelative`. | ||
| * | ||
| * Two-pass design: every ancestor is tried under the `explicitTrackingAttribute` | ||
| * strategy first, then the entire walk repeats with `stableId`. This ordering | ||
| * means an explicit anchor anywhere up the tree always beats a stable id deeper | ||
| * down — the design doc describes this as the "customer intent overrides | ||
| * structural inference" rule. | ||
| * | ||
| * The orchestrator does not own the fallback. When no strategy + walk produces | ||
| * a unique selector, it returns `null` and the engine wires the | ||
| * `fallback-css-path` path in instead. Keeping the fallback outside the | ||
| * orchestrator keeps the strategy chain testable in isolation. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { ElementSelectorLogger, ResolvedSelectorConfig, Strategy } from './types'; | ||
| /** | ||
| * Default strategy chain in priority order. Used by the engine factory when the | ||
| * caller doesn't provide an explicit list. Exported for diagnostics — tests | ||
| * import this so they don't have to duplicate the list. | ||
| */ | ||
| export declare const DEFAULT_STRATEGIES: ReadonlyArray<Strategy>; | ||
| export interface OrchestratorOptions { | ||
| /** Strategy chain. Defaults to `DEFAULT_STRATEGIES`. Order = priority. */ | ||
| strategies?: ReadonlyArray<Strategy>; | ||
| /** Document or shadow root used for uniqueness checks. Defaults to the target's owner document. */ | ||
| scope?: ParentNode; | ||
| /** | ||
| * Optional logger. When provided, the orchestrator emits a `debug` log if a | ||
| * strategy produces a malformed selector that throws during the uniqueness | ||
| * check — useful for diagnosing strategy bugs without crashing the engine. | ||
| */ | ||
| logger?: ElementSelectorLogger; | ||
| } | ||
| /** | ||
| * Try to produce a CSS selector for `el` using the strategy chain. | ||
| * | ||
| * Returns the composed selector string on success, or `null` if every strategy | ||
| * declined at every walked ancestor. Callers (the engine) treat `null` as the | ||
| * signal to invoke the fallback walker. | ||
| * | ||
| * Selector format on success: | ||
| * | ||
| * - Anchor only (target itself was an anchor): `<anchor-selector>` | ||
| * - Anchor + trail (anchor was an ancestor): `<anchor-selector> > <descent>` | ||
| * | ||
| * Where `<anchor-selector>` is whatever the winning strategy returned for the | ||
| * anchor element (e.g. `[data-amp-track-id="login-form"]` or `div#hero`), and | ||
| * `<descent>` is `describeRelative(anchor, trail)`. | ||
| */ | ||
| export declare function runOrchestrator(el: Element, config: ResolvedSelectorConfig, options?: OrchestratorOptions): string | null; | ||
| //# sourceMappingURL=orchestrator.d.ts.map |
| {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,QAAQ,EAAmB,MAAM,SAAS,CAAC;AAKnG;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,QAAQ,CAAiD,CAAC;AAEzG,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,UAAU,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACrC,mGAAmG;IACnG,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,sBAAsB,EAC9B,OAAO,GAAE,mBAAwB,GAChC,MAAM,GAAG,IAAI,CAoCf"} |
| "use strict"; | ||
| /** | ||
| * Orchestrator — the entry point for the v1 element-selector algorithm. | ||
| * | ||
| * Given a click target, the orchestrator walks from the target up through its | ||
| * ancestors and tries each registered strategy at each level. The first | ||
| * strategy that returns a non-null candidate (and produces a selector that | ||
| * resolves uniquely under the scope) wins; the trail of intermediate elements | ||
| * between the anchor and the original target is then expressed as a positional | ||
| * descent via `describeRelative`. | ||
| * | ||
| * Two-pass design: every ancestor is tried under the `explicitTrackingAttribute` | ||
| * strategy first, then the entire walk repeats with `stableId`. This ordering | ||
| * means an explicit anchor anywhere up the tree always beats a stable id deeper | ||
| * down — the design doc describes this as the "customer intent overrides | ||
| * structural inference" rule. | ||
| * | ||
| * The orchestrator does not own the fallback. When no strategy + walk produces | ||
| * a unique selector, it returns `null` and the engine wires the | ||
| * `fallback-css-path` path in instead. Keeping the fallback outside the | ||
| * orchestrator keeps the strategy chain testable in isolation. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.runOrchestrator = exports.DEFAULT_STRATEGIES = void 0; | ||
| var tslib_1 = require("tslib"); | ||
| var explicit_tracking_attribute_1 = require("./strategies/explicit-tracking-attribute"); | ||
| var stable_id_1 = require("./strategies/stable-id"); | ||
| var describe_relative_1 = require("./helpers/describe-relative"); | ||
| /** | ||
| * Default strategy chain in priority order. Used by the engine factory when the | ||
| * caller doesn't provide an explicit list. Exported for diagnostics — tests | ||
| * import this so they don't have to duplicate the list. | ||
| */ | ||
| exports.DEFAULT_STRATEGIES = [explicit_tracking_attribute_1.explicitTrackingAttribute, stable_id_1.stableId]; | ||
| /** | ||
| * Try to produce a CSS selector for `el` using the strategy chain. | ||
| * | ||
| * Returns the composed selector string on success, or `null` if every strategy | ||
| * declined at every walked ancestor. Callers (the engine) treat `null` as the | ||
| * signal to invoke the fallback walker. | ||
| * | ||
| * Selector format on success: | ||
| * | ||
| * - Anchor only (target itself was an anchor): `<anchor-selector>` | ||
| * - Anchor + trail (anchor was an ancestor): `<anchor-selector> > <descent>` | ||
| * | ||
| * Where `<anchor-selector>` is whatever the winning strategy returned for the | ||
| * anchor element (e.g. `[data-amp-track-id="login-form"]` or `div#hero`), and | ||
| * `<descent>` is `describeRelative(anchor, trail)`. | ||
| */ | ||
| function runOrchestrator(el, config, options) { | ||
| var e_1, _a; | ||
| var _b, _c, _d; | ||
| if (options === void 0) { options = {}; } | ||
| var strategies = (_b = options.strategies) !== null && _b !== void 0 ? _b : exports.DEFAULT_STRATEGIES; | ||
| var scope = (_d = (_c = options.scope) !== null && _c !== void 0 ? _c : el.ownerDocument) !== null && _d !== void 0 ? _d : document; | ||
| var ctx = { scope: scope, config: config }; | ||
| var walk = collectWalk(el, config.maxAncestorWalkDepth); | ||
| try { | ||
| // Two-pass: try strategy[0] across the whole walk, then strategy[1], etc. | ||
| // This gives an "explicit anchor at any depth beats a structural anchor any | ||
| // depth" priority, which matches the design doc's "customer intent overrides | ||
| // structural inference" rule. | ||
| for (var strategies_1 = tslib_1.__values(strategies), strategies_1_1 = strategies_1.next(); !strategies_1_1.done; strategies_1_1 = strategies_1.next()) { | ||
| var strategy = strategies_1_1.value; | ||
| for (var i = 0; i < walk.length; i++) { | ||
| var anchor = walk[i]; | ||
| var anchorSelector = strategy.try(anchor, ctx); | ||
| if (anchorSelector === null) | ||
| continue; | ||
| // Anchor was the target itself — no descent needed. | ||
| if (i === 0) { | ||
| if (isUniqueMatch(scope, anchorSelector, el, strategy.name, options.logger)) { | ||
| return anchorSelector; | ||
| } | ||
| continue; | ||
| } | ||
| // Anchor was an ancestor — descend back to the target through the trail. | ||
| var trail = walk.slice(0, i).reverse(); | ||
| var descent = (0, describe_relative_1.describeRelative)(anchor, trail); | ||
| var composed = "".concat(anchorSelector, " > ").concat(descent); | ||
| if (isUniqueMatch(scope, composed, el, strategy.name, options.logger)) { | ||
| return composed; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
| finally { | ||
| try { | ||
| if (strategies_1_1 && !strategies_1_1.done && (_a = strategies_1.return)) _a.call(strategies_1); | ||
| } | ||
| finally { if (e_1) throw e_1.error; } | ||
| } | ||
| return null; | ||
| } | ||
| exports.runOrchestrator = runOrchestrator; | ||
| /** | ||
| * Build the walk array `[target, parent, grandparent, ...]`, stopping at | ||
| * `<html>` (or at the depth limit if configured). Excludes the document | ||
| * element's parent (which is the Document node and not an Element). | ||
| */ | ||
| function collectWalk(el, maxDepth) { | ||
| var walk = []; | ||
| var cursor = el; | ||
| while (cursor !== null) { | ||
| walk.push(cursor); | ||
| if (maxDepth !== undefined && walk.length > maxDepth) | ||
| break; | ||
| cursor = cursor.parentElement; | ||
| } | ||
| return walk; | ||
| } | ||
| /** | ||
| * Check whether `selector` resolves to exactly `el` and no other element in | ||
| * `scope`. The strategies themselves don't run uniqueness checks — that's the | ||
| * orchestrator's job, so strategies stay pure transforms and stay testable in | ||
| * isolation. | ||
| * | ||
| * When the selector is malformed (which is a strategy bug rather than user | ||
| * input), we log at `debug` and return false rather than throwing. The log | ||
| * lets engineers diagnose a misbehaving strategy without the engine taking | ||
| * down whatever it's wired into. | ||
| */ | ||
| function isUniqueMatch(scope, selector, el, strategyName, logger) { | ||
| var matches; | ||
| try { | ||
| matches = scope.querySelectorAll(selector); | ||
| } | ||
| catch (e) { | ||
| var message = e instanceof Error ? e.message : String(e); | ||
| logger === null || logger === void 0 ? void 0 : logger.debug("@amplitude/element-selector: strategy \"".concat(strategyName, "\" emitted a malformed selector \"").concat(selector, "\" (").concat(message, ")")); | ||
| return false; | ||
| } | ||
| return matches.length === 1 && matches[0] === el; | ||
| } | ||
| //# sourceMappingURL=orchestrator.js.map |
| {"version":3,"file":"orchestrator.js","sourceRoot":"","sources":["../../src/orchestrator.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;;;;AAGH,wFAAqF;AACrF,oDAAsE;AACtE,iEAA+D;AAE/D;;;;GAIG;AACU,QAAA,kBAAkB,GAA4B,CAAC,uDAAyB,EAAE,oBAAgB,CAAC,CAAC;AAezG;;;;;;;;;;;;;;;GAeG;AACH,SAAgB,eAAe,CAC7B,EAAW,EACX,MAA8B,EAC9B,OAAiC;;;IAAjC,wBAAA,EAAA,YAAiC;IAEjC,IAAM,UAAU,GAAG,MAAA,OAAO,CAAC,UAAU,mCAAI,0BAAkB,CAAC;IAC5D,IAAM,KAAK,GAAe,MAAA,MAAA,OAAO,CAAC,KAAK,mCAAI,EAAE,CAAC,aAAa,mCAAI,QAAQ,CAAC;IACxE,IAAM,GAAG,GAAoB,EAAE,KAAK,OAAA,EAAE,MAAM,QAAA,EAAE,CAAC;IAE/C,IAAM,IAAI,GAAG,WAAW,CAAC,EAAE,EAAE,MAAM,CAAC,oBAAoB,CAAC,CAAC;;QAE1D,0EAA0E;QAC1E,4EAA4E;QAC5E,6EAA6E;QAC7E,8BAA8B;QAC9B,KAAuB,IAAA,eAAA,iBAAA,UAAU,CAAA,sCAAA,8DAAE;YAA9B,IAAM,QAAQ,uBAAA;YACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;gBACpC,IAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;gBACvB,IAAM,cAAc,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;gBACjD,IAAI,cAAc,KAAK,IAAI;oBAAE,SAAS;gBAEtC,oDAAoD;gBACpD,IAAI,CAAC,KAAK,CAAC,EAAE;oBACX,IAAI,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE;wBAC3E,OAAO,cAAc,CAAC;qBACvB;oBACD,SAAS;iBACV;gBAED,yEAAyE;gBACzE,IAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;gBACzC,IAAM,OAAO,GAAG,IAAA,oCAAgB,EAAC,MAAM,EAAE,KAAK,CAAC,CAAC;gBAChD,IAAM,QAAQ,GAAG,UAAG,cAAc,gBAAM,OAAO,CAAE,CAAC;gBAClD,IAAI,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE;oBACrE,OAAO,QAAQ,CAAC;iBACjB;aACF;SACF;;;;;;;;;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAxCD,0CAwCC;AAED;;;;GAIG;AACH,SAAS,WAAW,CAAC,EAAW,EAAE,QAA4B;IAC5D,IAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,IAAI,MAAM,GAAmB,EAAE,CAAC;IAChC,OAAO,MAAM,KAAK,IAAI,EAAE;QACtB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClB,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,GAAG,QAAQ;YAAE,MAAM;QAC5D,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC;KAC/B;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,aAAa,CACpB,KAAiB,EACjB,QAAgB,EAChB,EAAW,EACX,YAAoB,EACpB,MAA8B;IAE9B,IAAI,OAA4B,CAAC;IACjC,IAAI;QACF,OAAO,GAAG,KAAK,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;KAC5C;IAAC,OAAO,CAAC,EAAE;QACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,KAAK,CACX,kDAA0C,YAAY,+CAAmC,QAAQ,iBAAM,OAAO,MAAG,CAClH,CAAC;QACF,OAAO,KAAK,CAAC;KACd;IACD,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;AACnD,CAAC","sourcesContent":["/**\n * Orchestrator — the entry point for the v1 element-selector algorithm.\n *\n * Given a click target, the orchestrator walks from the target up through its\n * ancestors and tries each registered strategy at each level. The first\n * strategy that returns a non-null candidate (and produces a selector that\n * resolves uniquely under the scope) wins; the trail of intermediate elements\n * between the anchor and the original target is then expressed as a positional\n * descent via `describeRelative`.\n *\n * Two-pass design: every ancestor is tried under the `explicitTrackingAttribute`\n * strategy first, then the entire walk repeats with `stableId`. This ordering\n * means an explicit anchor anywhere up the tree always beats a stable id deeper\n * down — the design doc describes this as the \"customer intent overrides\n * structural inference\" rule.\n *\n * The orchestrator does not own the fallback. When no strategy + walk produces\n * a unique selector, it returns `null` and the engine wires the\n * `fallback-css-path` path in instead. Keeping the fallback outside the\n * orchestrator keeps the strategy chain testable in isolation.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ResolvedSelectorConfig, Strategy, StrategyContext } from './types';\nimport { explicitTrackingAttribute } from './strategies/explicit-tracking-attribute';\nimport { stableId as stableIdStrategy } from './strategies/stable-id';\nimport { describeRelative } from './helpers/describe-relative';\n\n/**\n * Default strategy chain in priority order. Used by the engine factory when the\n * caller doesn't provide an explicit list. Exported for diagnostics — tests\n * import this so they don't have to duplicate the list.\n */\nexport const DEFAULT_STRATEGIES: ReadonlyArray<Strategy> = [explicitTrackingAttribute, stableIdStrategy];\n\nexport interface OrchestratorOptions {\n /** Strategy chain. Defaults to `DEFAULT_STRATEGIES`. Order = priority. */\n strategies?: ReadonlyArray<Strategy>;\n /** Document or shadow root used for uniqueness checks. Defaults to the target's owner document. */\n scope?: ParentNode;\n /**\n * Optional logger. When provided, the orchestrator emits a `debug` log if a\n * strategy produces a malformed selector that throws during the uniqueness\n * check — useful for diagnosing strategy bugs without crashing the engine.\n */\n logger?: ElementSelectorLogger;\n}\n\n/**\n * Try to produce a CSS selector for `el` using the strategy chain.\n *\n * Returns the composed selector string on success, or `null` if every strategy\n * declined at every walked ancestor. Callers (the engine) treat `null` as the\n * signal to invoke the fallback walker.\n *\n * Selector format on success:\n *\n * - Anchor only (target itself was an anchor): `<anchor-selector>`\n * - Anchor + trail (anchor was an ancestor): `<anchor-selector> > <descent>`\n *\n * Where `<anchor-selector>` is whatever the winning strategy returned for the\n * anchor element (e.g. `[data-amp-track-id=\"login-form\"]` or `div#hero`), and\n * `<descent>` is `describeRelative(anchor, trail)`.\n */\nexport function runOrchestrator(\n el: Element,\n config: ResolvedSelectorConfig,\n options: OrchestratorOptions = {},\n): string | null {\n const strategies = options.strategies ?? DEFAULT_STRATEGIES;\n const scope: ParentNode = options.scope ?? el.ownerDocument ?? document;\n const ctx: StrategyContext = { scope, config };\n\n const walk = collectWalk(el, config.maxAncestorWalkDepth);\n\n // Two-pass: try strategy[0] across the whole walk, then strategy[1], etc.\n // This gives an \"explicit anchor at any depth beats a structural anchor any\n // depth\" priority, which matches the design doc's \"customer intent overrides\n // structural inference\" rule.\n for (const strategy of strategies) {\n for (let i = 0; i < walk.length; i++) {\n const anchor = walk[i];\n const anchorSelector = strategy.try(anchor, ctx);\n if (anchorSelector === null) continue;\n\n // Anchor was the target itself — no descent needed.\n if (i === 0) {\n if (isUniqueMatch(scope, anchorSelector, el, strategy.name, options.logger)) {\n return anchorSelector;\n }\n continue;\n }\n\n // Anchor was an ancestor — descend back to the target through the trail.\n const trail = walk.slice(0, i).reverse();\n const descent = describeRelative(anchor, trail);\n const composed = `${anchorSelector} > ${descent}`;\n if (isUniqueMatch(scope, composed, el, strategy.name, options.logger)) {\n return composed;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Build the walk array `[target, parent, grandparent, ...]`, stopping at\n * `<html>` (or at the depth limit if configured). Excludes the document\n * element's parent (which is the Document node and not an Element).\n */\nfunction collectWalk(el: Element, maxDepth: number | undefined): Element[] {\n const walk: Element[] = [];\n let cursor: Element | null = el;\n while (cursor !== null) {\n walk.push(cursor);\n if (maxDepth !== undefined && walk.length > maxDepth) break;\n cursor = cursor.parentElement;\n }\n return walk;\n}\n\n/**\n * Check whether `selector` resolves to exactly `el` and no other element in\n * `scope`. The strategies themselves don't run uniqueness checks — that's the\n * orchestrator's job, so strategies stay pure transforms and stay testable in\n * isolation.\n *\n * When the selector is malformed (which is a strategy bug rather than user\n * input), we log at `debug` and return false rather than throwing. The log\n * lets engineers diagnose a misbehaving strategy without the engine taking\n * down whatever it's wired into.\n */\nfunction isUniqueMatch(\n scope: ParentNode,\n selector: string,\n el: Element,\n strategyName: string,\n logger?: ElementSelectorLogger,\n): boolean {\n let matches: NodeListOf<Element>;\n try {\n matches = scope.querySelectorAll(selector);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.debug(\n `@amplitude/element-selector: strategy \"${strategyName}\" emitted a malformed selector \"${selector}\" (${message})`,\n );\n return false;\n }\n return matches.length === 1 && matches[0] === el;\n}\n"]} |
| import { ElementSelectorLogger } from '../types'; | ||
| /** | ||
| * Autogenerated-id pattern pack. | ||
| * | ||
| * When an element id matches any of these regexes, the algorithm treats the | ||
| * element as if it has no id — the stableId strategy returns null and the | ||
| * fallback walks past it instead of anchoring on it. Stops selectors from | ||
| * pinning to ids that change between page loads or component re-mounts. | ||
| * | ||
| * Defaults are surfaced to customers in remote config so they can audit what's | ||
| * being filtered and add or remove patterns to match their stack. | ||
| */ | ||
| /** | ||
| * Built-in defaults, in the order they're applied (order doesn't affect | ||
| * correctness — `some()` short-circuits on the first match). | ||
| */ | ||
| export declare const DEFAULT_AUTOGENERATED_ID_PATTERNS: ReadonlyArray<RegExp>; | ||
| /** | ||
| * Compile a list of regex pattern strings (e.g. from a remote-config payload) | ||
| * into RegExp objects. Invalid patterns are skipped (not thrown) so a single | ||
| * bad regex in remote config can't crash the engine. | ||
| * | ||
| * Pass `logger` to surface invalid patterns at `warn` level — useful for | ||
| * diagnosing why a customer's override isn't taking effect. | ||
| * | ||
| * Callers decide whether to merge the result with `DEFAULT_AUTOGENERATED_ID_PATTERNS` | ||
| * or use it as a full replacement. That policy lives in the config resolver, | ||
| * not here. | ||
| */ | ||
| export declare function compile(patterns: string[], logger?: ElementSelectorLogger): RegExp[]; | ||
| /** | ||
| * Check whether `id` is "stable" relative to a set of patterns. An id is | ||
| * stable when it's a non-empty string AND doesn't match any pattern in | ||
| * `patterns`. Null, undefined, and empty-string ids are not stable. | ||
| */ | ||
| export declare function isStableId(id: string | null | undefined, patterns: ReadonlyArray<RegExp>): boolean; | ||
| //# sourceMappingURL=autogenerated-ids.d.ts.map |
| {"version":3,"file":"autogenerated-ids.d.ts","sourceRoot":"","sources":["../../../src/patterns/autogenerated-ids.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,eAAO,MAAM,iCAAiC,EAAE,aAAa,CAAC,MAAM,CAkBnE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAWpF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAMlG"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.isStableId = exports.compile = exports.DEFAULT_AUTOGENERATED_ID_PATTERNS = void 0; | ||
| var tslib_1 = require("tslib"); | ||
| /** | ||
| * Autogenerated-id pattern pack. | ||
| * | ||
| * When an element id matches any of these regexes, the algorithm treats the | ||
| * element as if it has no id — the stableId strategy returns null and the | ||
| * fallback walks past it instead of anchoring on it. Stops selectors from | ||
| * pinning to ids that change between page loads or component re-mounts. | ||
| * | ||
| * Defaults are surfaced to customers in remote config so they can audit what's | ||
| * being filtered and add or remove patterns to match their stack. | ||
| */ | ||
| /** | ||
| * Built-in defaults, in the order they're applied (order doesn't affect | ||
| * correctness — `some()` short-circuits on the first match). | ||
| */ | ||
| exports.DEFAULT_AUTOGENERATED_ID_PATTERNS = [ | ||
| // React useId() — ":r0:", ":r1:", ":rk:". Stable for one render; new id on next mount. | ||
| /^:r[0-9a-z]+:$/, | ||
| // Radix UI primitives — "radix-:r3:", "radix-A1B2C3". Prefix match (the | ||
| // shape of what comes after `radix-` varies by Radix version and React | ||
| // major; only the prefix is reliable). | ||
| /^radix-/, | ||
| // Headless UI (Tailwind Labs) — "headlessui-menu-button-:r5:". Prefix match. | ||
| /^headlessui-/, | ||
| // MUI internal id prefix — "mui-12345". Don't confuse with Mui-* CSS class state markers. | ||
| /^mui-/, | ||
| // Hex-only ids >=16 chars — UUIDs without hyphens, build hashes. Whole-string match. | ||
| /^[a-f0-9]{16,}$/i, | ||
| // Trailing long digit run — "session-1700000000", "row-20250515". Timestamps, counters. | ||
| /-\d{8,}$/, | ||
| // Any 4+ consecutive digits anywhere — "product-2134", "swiper-wrapper-…41039". | ||
| // ContentSquare's heuristic; catches the digit-heavy portions of library-generated ids. | ||
| /\d{4,}/, | ||
| ]; | ||
| /** | ||
| * Compile a list of regex pattern strings (e.g. from a remote-config payload) | ||
| * into RegExp objects. Invalid patterns are skipped (not thrown) so a single | ||
| * bad regex in remote config can't crash the engine. | ||
| * | ||
| * Pass `logger` to surface invalid patterns at `warn` level — useful for | ||
| * diagnosing why a customer's override isn't taking effect. | ||
| * | ||
| * Callers decide whether to merge the result with `DEFAULT_AUTOGENERATED_ID_PATTERNS` | ||
| * or use it as a full replacement. That policy lives in the config resolver, | ||
| * not here. | ||
| */ | ||
| function compile(patterns, logger) { | ||
| var e_1, _a; | ||
| var compiled = []; | ||
| try { | ||
| for (var patterns_1 = tslib_1.__values(patterns), patterns_1_1 = patterns_1.next(); !patterns_1_1.done; patterns_1_1 = patterns_1.next()) { | ||
| var pattern = patterns_1_1.value; | ||
| try { | ||
| compiled.push(new RegExp(pattern)); | ||
| } | ||
| catch (e) { | ||
| var message = e instanceof Error ? e.message : String(e); | ||
| logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: ignoring invalid autogenerated-id pattern \"".concat(pattern, "\" (").concat(message, ")")); | ||
| } | ||
| } | ||
| } | ||
| catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
| finally { | ||
| try { | ||
| if (patterns_1_1 && !patterns_1_1.done && (_a = patterns_1.return)) _a.call(patterns_1); | ||
| } | ||
| finally { if (e_1) throw e_1.error; } | ||
| } | ||
| return compiled; | ||
| } | ||
| exports.compile = compile; | ||
| /** | ||
| * Check whether `id` is "stable" relative to a set of patterns. An id is | ||
| * stable when it's a non-empty string AND doesn't match any pattern in | ||
| * `patterns`. Null, undefined, and empty-string ids are not stable. | ||
| */ | ||
| function isStableId(id, patterns) { | ||
| var e_2, _a; | ||
| if (id === null || id === undefined || id === '') | ||
| return false; | ||
| try { | ||
| for (var patterns_2 = tslib_1.__values(patterns), patterns_2_1 = patterns_2.next(); !patterns_2_1.done; patterns_2_1 = patterns_2.next()) { | ||
| var pattern = patterns_2_1.value; | ||
| if (pattern.test(id)) | ||
| return false; | ||
| } | ||
| } | ||
| catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
| finally { | ||
| try { | ||
| if (patterns_2_1 && !patterns_2_1.done && (_a = patterns_2.return)) _a.call(patterns_2); | ||
| } | ||
| finally { if (e_2) throw e_2.error; } | ||
| } | ||
| return true; | ||
| } | ||
| exports.isStableId = isStableId; | ||
| //# sourceMappingURL=autogenerated-ids.js.map |
| {"version":3,"file":"autogenerated-ids.js","sourceRoot":"","sources":["../../../src/patterns/autogenerated-ids.ts"],"names":[],"mappings":";;;;AAEA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACU,QAAA,iCAAiC,GAA0B;IACtE,uFAAuF;IACvF,gBAAgB;IAChB,wEAAwE;IACxE,uEAAuE;IACvE,uCAAuC;IACvC,SAAS;IACT,6EAA6E;IAC7E,cAAc;IACd,0FAA0F;IAC1F,OAAO;IACP,qFAAqF;IACrF,kBAAkB;IAClB,wFAAwF;IACxF,UAAU;IACV,gFAAgF;IAChF,wFAAwF;IACxF,QAAQ;CACT,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,SAAgB,OAAO,CAAC,QAAkB,EAAE,MAA8B;;IACxE,IAAM,QAAQ,GAAa,EAAE,CAAC;;QAC9B,KAAsB,IAAA,aAAA,iBAAA,QAAQ,CAAA,kCAAA,wDAAE;YAA3B,IAAM,OAAO,qBAAA;YAChB,IAAI;gBACF,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;aACpC;YAAC,OAAO,CAAC,EAAE;gBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,mFAA2E,OAAO,iBAAM,OAAO,MAAG,CAAC,CAAC;aAClH;SACF;;;;;;;;;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAXD,0BAWC;AAED;;;;GAIG;AACH,SAAgB,UAAU,CAAC,EAA6B,EAAE,QAA+B;;IACvF,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;;QAC/D,KAAsB,IAAA,aAAA,iBAAA,QAAQ,CAAA,kCAAA,wDAAE;YAA3B,IAAM,OAAO,qBAAA;YAChB,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBAAE,OAAO,KAAK,CAAC;SACpC;;;;;;;;;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAND,gCAMC","sourcesContent":["import { ElementSelectorLogger } from '../types';\n\n/**\n * Autogenerated-id pattern pack.\n *\n * When an element id matches any of these regexes, the algorithm treats the\n * element as if it has no id — the stableId strategy returns null and the\n * fallback walks past it instead of anchoring on it. Stops selectors from\n * pinning to ids that change between page loads or component re-mounts.\n *\n * Defaults are surfaced to customers in remote config so they can audit what's\n * being filtered and add or remove patterns to match their stack.\n */\n\n/**\n * Built-in defaults, in the order they're applied (order doesn't affect\n * correctness — `some()` short-circuits on the first match).\n */\nexport const DEFAULT_AUTOGENERATED_ID_PATTERNS: ReadonlyArray<RegExp> = [\n // React useId() — \":r0:\", \":r1:\", \":rk:\". Stable for one render; new id on next mount.\n /^:r[0-9a-z]+:$/,\n // Radix UI primitives — \"radix-:r3:\", \"radix-A1B2C3\". Prefix match (the\n // shape of what comes after `radix-` varies by Radix version and React\n // major; only the prefix is reliable).\n /^radix-/,\n // Headless UI (Tailwind Labs) — \"headlessui-menu-button-:r5:\". Prefix match.\n /^headlessui-/,\n // MUI internal id prefix — \"mui-12345\". Don't confuse with Mui-* CSS class state markers.\n /^mui-/,\n // Hex-only ids >=16 chars — UUIDs without hyphens, build hashes. Whole-string match.\n /^[a-f0-9]{16,}$/i,\n // Trailing long digit run — \"session-1700000000\", \"row-20250515\". Timestamps, counters.\n /-\\d{8,}$/,\n // Any 4+ consecutive digits anywhere — \"product-2134\", \"swiper-wrapper-…41039\".\n // ContentSquare's heuristic; catches the digit-heavy portions of library-generated ids.\n /\\d{4,}/,\n];\n\n/**\n * Compile a list of regex pattern strings (e.g. from a remote-config payload)\n * into RegExp objects. Invalid patterns are skipped (not thrown) so a single\n * bad regex in remote config can't crash the engine.\n *\n * Pass `logger` to surface invalid patterns at `warn` level — useful for\n * diagnosing why a customer's override isn't taking effect.\n *\n * Callers decide whether to merge the result with `DEFAULT_AUTOGENERATED_ID_PATTERNS`\n * or use it as a full replacement. That policy lives in the config resolver,\n * not here.\n */\nexport function compile(patterns: string[], logger?: ElementSelectorLogger): RegExp[] {\n const compiled: RegExp[] = [];\n for (const pattern of patterns) {\n try {\n compiled.push(new RegExp(pattern));\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: ignoring invalid autogenerated-id pattern \"${pattern}\" (${message})`);\n }\n }\n return compiled;\n}\n\n/**\n * Check whether `id` is \"stable\" relative to a set of patterns. An id is\n * stable when it's a non-empty string AND doesn't match any pattern in\n * `patterns`. Null, undefined, and empty-string ids are not stable.\n */\nexport function isStableId(id: string | null | undefined, patterns: ReadonlyArray<RegExp>): boolean {\n if (id === null || id === undefined || id === '') return false;\n for (const pattern of patterns) {\n if (pattern.test(id)) return false;\n }\n return true;\n}\n"]} |
| import { ElementSelectorLogger } from '../types'; | ||
| /** | ||
| * Unstable-class pattern pack. | ||
| * | ||
| * The fallback walker filters classes through these regexes before adding them | ||
| * to a selector. Classes that match are dropped — they don't participate in | ||
| * sibling disambiguation and never appear in the emitted output. Defends | ||
| * against three failure modes: | ||
| * | ||
| * 1. Build-tool / framework utilities (Tailwind, etc.) — class names that | ||
| * look stable but change with every design tweak. | ||
| * 2. CSS-in-JS / build-hash classes (Emotion, CSS modules, styled-components, | ||
| * styled-jsx) — change on every build. | ||
| * 3. Library runtime state classes (Swiper, MUI, Radix, Headless UI, | ||
| * BEM-style is-active/is-open) — class is stable in name but its presence | ||
| * on a given element moves as the user interacts. | ||
| * | ||
| * Defaults are surfaced to customers in remote config so they can audit what's | ||
| * being filtered and add or remove patterns to match their stack. | ||
| */ | ||
| /** | ||
| * Built-in defaults grouped by category for readability. The runtime treats | ||
| * the whole list uniformly via `Array.prototype.some(pattern.test)`. | ||
| */ | ||
| export declare const DEFAULT_UNSTABLE_CLASS_PATTERNS: ReadonlyArray<RegExp>; | ||
| /** | ||
| * Compile a list of regex pattern strings (e.g. from a remote-config payload) | ||
| * into RegExp objects. Invalid patterns are skipped (not thrown) so a single | ||
| * bad regex in remote config can't crash the engine. | ||
| * | ||
| * Pass `logger` to surface invalid patterns at `warn` level — useful for | ||
| * diagnosing why a customer's override isn't taking effect. | ||
| * | ||
| * Callers decide whether to merge the result with `DEFAULT_UNSTABLE_CLASS_PATTERNS` | ||
| * or use it as a full replacement. That policy lives in the config resolver, | ||
| * not here. | ||
| */ | ||
| export declare function compile(patterns: string[], logger?: ElementSelectorLogger): RegExp[]; | ||
| /** | ||
| * Filter a list of class names, dropping any that match a pattern. Returns a | ||
| * new array containing only the survivors, preserving original order. | ||
| * | ||
| * Used by the fallback walker (in the next PR) before adding classes to a | ||
| * selector for sibling disambiguation. Also returns sensibly on null / | ||
| * undefined / empty inputs. | ||
| */ | ||
| export declare function filterClasses(classes: string[], patterns: ReadonlyArray<RegExp>): string[]; | ||
| //# sourceMappingURL=unstable-classes.d.ts.map |
| {"version":3,"file":"unstable-classes.d.ts","sourceRoot":"","sources":["../../../src/patterns/unstable-classes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACH,eAAO,MAAM,+BAA+B,EAAE,aAAa,CAAC,MAAM,CA6CjE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAWpF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAe1F"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.filterClasses = exports.compile = exports.DEFAULT_UNSTABLE_CLASS_PATTERNS = void 0; | ||
| var tslib_1 = require("tslib"); | ||
| /** | ||
| * Unstable-class pattern pack. | ||
| * | ||
| * The fallback walker filters classes through these regexes before adding them | ||
| * to a selector. Classes that match are dropped — they don't participate in | ||
| * sibling disambiguation and never appear in the emitted output. Defends | ||
| * against three failure modes: | ||
| * | ||
| * 1. Build-tool / framework utilities (Tailwind, etc.) — class names that | ||
| * look stable but change with every design tweak. | ||
| * 2. CSS-in-JS / build-hash classes (Emotion, CSS modules, styled-components, | ||
| * styled-jsx) — change on every build. | ||
| * 3. Library runtime state classes (Swiper, MUI, Radix, Headless UI, | ||
| * BEM-style is-active/is-open) — class is stable in name but its presence | ||
| * on a given element moves as the user interacts. | ||
| * | ||
| * Defaults are surfaced to customers in remote config so they can audit what's | ||
| * being filtered and add or remove patterns to match their stack. | ||
| */ | ||
| /** | ||
| * Built-in defaults grouped by category for readability. The runtime treats | ||
| * the whole list uniformly via `Array.prototype.some(pattern.test)`. | ||
| */ | ||
| exports.DEFAULT_UNSTABLE_CLASS_PATTERNS = [ | ||
| // ===== Tailwind / build-tool utilities ===== | ||
| // Tailwind spacing: p-4, px-2, py-8, pt-1, mt-4, etc. | ||
| /^(p|m|px|py|mx|my|pt|pb|pl|pr|mt|mb|ml|mr)-\d+$/, | ||
| // Tailwind sizing: w-full, h-screen, max-w-[1440px], etc. | ||
| /^(w|h|min-w|max-w|min-h|max-h)-/, | ||
| // Tailwind color / visual: bg-blue-500, text-white, border-gray-200, ring-2, etc. | ||
| /^(text|bg|border|ring|fill|stroke)-/, | ||
| // Tailwind state variants: hover:underline, focus:ring-2, active:bg-transparent. | ||
| /^(hover|focus|active|disabled|group-hover):/, | ||
| // Tailwind breakpoint variants: md:flex, lg:grid-cols-3. | ||
| /^(sm|md|lg|xl|2xl):/, | ||
| // Tailwind z-index utility: z-10, z-50. | ||
| /^z-\d+$/, | ||
| // Tailwind arbitrary data-attribute variants: data-[state=open]:bg-white. | ||
| /^data-\[/, | ||
| // Tailwind arbitrary selector variants: [&_.swiper-slide]:h-auto, [&>div]:p-4. | ||
| /^\[/, | ||
| // ===== CSS-in-JS / build hashes ===== | ||
| // Emotion: css-1abcd23, css-9xyzkw0. | ||
| /^css-[a-z0-9]{6,}$/, | ||
| // CSS modules: Button_root__abc123, Card_container__xyz789. The middle | ||
| // segment can be as short as `root` (4 chars) in practice; the trailing | ||
| // hash is the load-bearing part. | ||
| /^[a-zA-Z]+_[a-zA-Z0-9]{3,}__[a-zA-Z0-9]{5,}$/, | ||
| // styled-components: sc-bdVaJa, sc-1jjuPXC0. | ||
| /^sc-[a-zA-Z0-9]{6,}$/, | ||
| // styled-jsx (Next.js): jsx-1234567. | ||
| /^jsx-\d+$/, | ||
| // ===== Library runtime state classes ===== | ||
| // Swiper carousel slide states. Move between elements as carousel advances. | ||
| /^swiper-slide-(visible|fully-visible|active|prev|next|duplicate)$/, | ||
| // BEM-style interaction state: is-active, is-open, is-selected, etc. | ||
| /^is-(active|open|selected|hovered|focused|expanded)$/, | ||
| // MUI per-component state: MuiButton-focusVisible, MuiSwitch-checked, etc. | ||
| /^Mui[A-Z][a-zA-Z]+-(focused|selected|disabled|expanded|focusVisible|active|checked)$/, | ||
| // MUI bare state classes: Mui-selected, Mui-disabled. | ||
| /^Mui-(selected|focused|disabled|expanded|focusVisible|active|checked)$/, | ||
| // Radix-style state class mirrors: data-state-open, data-state-checked. | ||
| /^data-state-/, | ||
| ]; | ||
| /** | ||
| * Compile a list of regex pattern strings (e.g. from a remote-config payload) | ||
| * into RegExp objects. Invalid patterns are skipped (not thrown) so a single | ||
| * bad regex in remote config can't crash the engine. | ||
| * | ||
| * Pass `logger` to surface invalid patterns at `warn` level — useful for | ||
| * diagnosing why a customer's override isn't taking effect. | ||
| * | ||
| * Callers decide whether to merge the result with `DEFAULT_UNSTABLE_CLASS_PATTERNS` | ||
| * or use it as a full replacement. That policy lives in the config resolver, | ||
| * not here. | ||
| */ | ||
| function compile(patterns, logger) { | ||
| var e_1, _a; | ||
| var compiled = []; | ||
| try { | ||
| for (var patterns_1 = tslib_1.__values(patterns), patterns_1_1 = patterns_1.next(); !patterns_1_1.done; patterns_1_1 = patterns_1.next()) { | ||
| var pattern = patterns_1_1.value; | ||
| try { | ||
| compiled.push(new RegExp(pattern)); | ||
| } | ||
| catch (e) { | ||
| var message = e instanceof Error ? e.message : String(e); | ||
| logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: ignoring invalid unstable-class pattern \"".concat(pattern, "\" (").concat(message, ")")); | ||
| } | ||
| } | ||
| } | ||
| catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
| finally { | ||
| try { | ||
| if (patterns_1_1 && !patterns_1_1.done && (_a = patterns_1.return)) _a.call(patterns_1); | ||
| } | ||
| finally { if (e_1) throw e_1.error; } | ||
| } | ||
| return compiled; | ||
| } | ||
| exports.compile = compile; | ||
| /** | ||
| * Filter a list of class names, dropping any that match a pattern. Returns a | ||
| * new array containing only the survivors, preserving original order. | ||
| * | ||
| * Used by the fallback walker (in the next PR) before adding classes to a | ||
| * selector for sibling disambiguation. Also returns sensibly on null / | ||
| * undefined / empty inputs. | ||
| */ | ||
| function filterClasses(classes, patterns) { | ||
| var e_2, _a, e_3, _b; | ||
| if (!classes || classes.length === 0) | ||
| return []; | ||
| var survivors = []; | ||
| try { | ||
| for (var classes_1 = tslib_1.__values(classes), classes_1_1 = classes_1.next(); !classes_1_1.done; classes_1_1 = classes_1.next()) { | ||
| var cls = classes_1_1.value; | ||
| if (!cls) | ||
| continue; | ||
| var matched = false; | ||
| try { | ||
| for (var patterns_2 = (e_3 = void 0, tslib_1.__values(patterns)), patterns_2_1 = patterns_2.next(); !patterns_2_1.done; patterns_2_1 = patterns_2.next()) { | ||
| var pattern = patterns_2_1.value; | ||
| if (pattern.test(cls)) { | ||
| matched = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
| finally { | ||
| try { | ||
| if (patterns_2_1 && !patterns_2_1.done && (_b = patterns_2.return)) _b.call(patterns_2); | ||
| } | ||
| finally { if (e_3) throw e_3.error; } | ||
| } | ||
| if (!matched) | ||
| survivors.push(cls); | ||
| } | ||
| } | ||
| catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
| finally { | ||
| try { | ||
| if (classes_1_1 && !classes_1_1.done && (_a = classes_1.return)) _a.call(classes_1); | ||
| } | ||
| finally { if (e_2) throw e_2.error; } | ||
| } | ||
| return survivors; | ||
| } | ||
| exports.filterClasses = filterClasses; | ||
| //# sourceMappingURL=unstable-classes.js.map |
| {"version":3,"file":"unstable-classes.js","sourceRoot":"","sources":["../../../src/patterns/unstable-classes.ts"],"names":[],"mappings":";;;;AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACU,QAAA,+BAA+B,GAA0B;IACpE,8CAA8C;IAE9C,sDAAsD;IACtD,iDAAiD;IACjD,0DAA0D;IAC1D,iCAAiC;IACjC,kFAAkF;IAClF,qCAAqC;IACrC,iFAAiF;IACjF,6CAA6C;IAC7C,yDAAyD;IACzD,qBAAqB;IACrB,wCAAwC;IACxC,SAAS;IACT,0EAA0E;IAC1E,UAAU;IACV,+EAA+E;IAC/E,KAAK;IAEL,uCAAuC;IAEvC,qCAAqC;IACrC,oBAAoB;IACpB,uEAAuE;IACvE,wEAAwE;IACxE,iCAAiC;IACjC,8CAA8C;IAC9C,6CAA6C;IAC7C,sBAAsB;IACtB,qCAAqC;IACrC,WAAW;IAEX,4CAA4C;IAE5C,4EAA4E;IAC5E,mEAAmE;IACnE,qEAAqE;IACrE,sDAAsD;IACtD,2EAA2E;IAC3E,sFAAsF;IACtF,sDAAsD;IACtD,wEAAwE;IACxE,wEAAwE;IACxE,cAAc;CACf,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,SAAgB,OAAO,CAAC,QAAkB,EAAE,MAA8B;;IACxE,IAAM,QAAQ,GAAa,EAAE,CAAC;;QAC9B,KAAsB,IAAA,aAAA,iBAAA,QAAQ,CAAA,kCAAA,wDAAE;YAA3B,IAAM,OAAO,qBAAA;YAChB,IAAI;gBACF,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;aACpC;YAAC,OAAO,CAAC,EAAE;gBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,iFAAyE,OAAO,iBAAM,OAAO,MAAG,CAAC,CAAC;aAChH;SACF;;;;;;;;;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAXD,0BAWC;AAED;;;;;;;GAOG;AACH,SAAgB,aAAa,CAAC,OAAiB,EAAE,QAA+B;;IAC9E,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChD,IAAM,SAAS,GAAa,EAAE,CAAC;;QAC/B,KAAkB,IAAA,YAAA,iBAAA,OAAO,CAAA,gCAAA,qDAAE;YAAtB,IAAM,GAAG,oBAAA;YACZ,IAAI,CAAC,GAAG;gBAAE,SAAS;YACnB,IAAI,OAAO,GAAG,KAAK,CAAC;;gBACpB,KAAsB,IAAA,4BAAA,iBAAA,QAAQ,CAAA,CAAA,kCAAA,wDAAE;oBAA3B,IAAM,OAAO,qBAAA;oBAChB,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;wBACrB,OAAO,GAAG,IAAI,CAAC;wBACf,MAAM;qBACP;iBACF;;;;;;;;;YACD,IAAI,CAAC,OAAO;gBAAE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;SACnC;;;;;;;;;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAfD,sCAeC","sourcesContent":["import { ElementSelectorLogger } from '../types';\n\n/**\n * Unstable-class pattern pack.\n *\n * The fallback walker filters classes through these regexes before adding them\n * to a selector. Classes that match are dropped — they don't participate in\n * sibling disambiguation and never appear in the emitted output. Defends\n * against three failure modes:\n *\n * 1. Build-tool / framework utilities (Tailwind, etc.) — class names that\n * look stable but change with every design tweak.\n * 2. CSS-in-JS / build-hash classes (Emotion, CSS modules, styled-components,\n * styled-jsx) — change on every build.\n * 3. Library runtime state classes (Swiper, MUI, Radix, Headless UI,\n * BEM-style is-active/is-open) — class is stable in name but its presence\n * on a given element moves as the user interacts.\n *\n * Defaults are surfaced to customers in remote config so they can audit what's\n * being filtered and add or remove patterns to match their stack.\n */\n\n/**\n * Built-in defaults grouped by category for readability. The runtime treats\n * the whole list uniformly via `Array.prototype.some(pattern.test)`.\n */\nexport const DEFAULT_UNSTABLE_CLASS_PATTERNS: ReadonlyArray<RegExp> = [\n // ===== Tailwind / build-tool utilities =====\n\n // Tailwind spacing: p-4, px-2, py-8, pt-1, mt-4, etc.\n /^(p|m|px|py|mx|my|pt|pb|pl|pr|mt|mb|ml|mr)-\\d+$/,\n // Tailwind sizing: w-full, h-screen, max-w-[1440px], etc.\n /^(w|h|min-w|max-w|min-h|max-h)-/,\n // Tailwind color / visual: bg-blue-500, text-white, border-gray-200, ring-2, etc.\n /^(text|bg|border|ring|fill|stroke)-/,\n // Tailwind state variants: hover:underline, focus:ring-2, active:bg-transparent.\n /^(hover|focus|active|disabled|group-hover):/,\n // Tailwind breakpoint variants: md:flex, lg:grid-cols-3.\n /^(sm|md|lg|xl|2xl):/,\n // Tailwind z-index utility: z-10, z-50.\n /^z-\\d+$/,\n // Tailwind arbitrary data-attribute variants: data-[state=open]:bg-white.\n /^data-\\[/,\n // Tailwind arbitrary selector variants: [&_.swiper-slide]:h-auto, [&>div]:p-4.\n /^\\[/,\n\n // ===== CSS-in-JS / build hashes =====\n\n // Emotion: css-1abcd23, css-9xyzkw0.\n /^css-[a-z0-9]{6,}$/,\n // CSS modules: Button_root__abc123, Card_container__xyz789. The middle\n // segment can be as short as `root` (4 chars) in practice; the trailing\n // hash is the load-bearing part.\n /^[a-zA-Z]+_[a-zA-Z0-9]{3,}__[a-zA-Z0-9]{5,}$/,\n // styled-components: sc-bdVaJa, sc-1jjuPXC0.\n /^sc-[a-zA-Z0-9]{6,}$/,\n // styled-jsx (Next.js): jsx-1234567.\n /^jsx-\\d+$/,\n\n // ===== Library runtime state classes =====\n\n // Swiper carousel slide states. Move between elements as carousel advances.\n /^swiper-slide-(visible|fully-visible|active|prev|next|duplicate)$/,\n // BEM-style interaction state: is-active, is-open, is-selected, etc.\n /^is-(active|open|selected|hovered|focused|expanded)$/,\n // MUI per-component state: MuiButton-focusVisible, MuiSwitch-checked, etc.\n /^Mui[A-Z][a-zA-Z]+-(focused|selected|disabled|expanded|focusVisible|active|checked)$/,\n // MUI bare state classes: Mui-selected, Mui-disabled.\n /^Mui-(selected|focused|disabled|expanded|focusVisible|active|checked)$/,\n // Radix-style state class mirrors: data-state-open, data-state-checked.\n /^data-state-/,\n];\n\n/**\n * Compile a list of regex pattern strings (e.g. from a remote-config payload)\n * into RegExp objects. Invalid patterns are skipped (not thrown) so a single\n * bad regex in remote config can't crash the engine.\n *\n * Pass `logger` to surface invalid patterns at `warn` level — useful for\n * diagnosing why a customer's override isn't taking effect.\n *\n * Callers decide whether to merge the result with `DEFAULT_UNSTABLE_CLASS_PATTERNS`\n * or use it as a full replacement. That policy lives in the config resolver,\n * not here.\n */\nexport function compile(patterns: string[], logger?: ElementSelectorLogger): RegExp[] {\n const compiled: RegExp[] = [];\n for (const pattern of patterns) {\n try {\n compiled.push(new RegExp(pattern));\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: ignoring invalid unstable-class pattern \"${pattern}\" (${message})`);\n }\n }\n return compiled;\n}\n\n/**\n * Filter a list of class names, dropping any that match a pattern. Returns a\n * new array containing only the survivors, preserving original order.\n *\n * Used by the fallback walker (in the next PR) before adding classes to a\n * selector for sibling disambiguation. Also returns sensibly on null /\n * undefined / empty inputs.\n */\nexport function filterClasses(classes: string[], patterns: ReadonlyArray<RegExp>): string[] {\n if (!classes || classes.length === 0) return [];\n const survivors: string[] = [];\n for (const cls of classes) {\n if (!cls) continue;\n let matched = false;\n for (const pattern of patterns) {\n if (pattern.test(cls)) {\n matched = true;\n break;\n }\n }\n if (!matched) survivors.push(cls);\n }\n return survivors;\n}\n"]} |
| import { Strategy } from '../types'; | ||
| /** | ||
| * First strategy in the chain. Customer-controlled anchor selector via the | ||
| * configured tracking attribute (default: `data-amp-track-id`). | ||
| * | ||
| * Semantics: | ||
| * | ||
| * - Attribute set with a non-empty value: | ||
| * returns `[<attr>="<value>"]` — uses this element as an explicit anchor. | ||
| * - Attribute set with an empty value: | ||
| * returns null. The empty value is a suppression signal for downstream | ||
| * components (the `stableId` strategy and the fallback both consult | ||
| * `getStableId` which honors the empty-value semantic). This strategy | ||
| * doesn't anchor on the element either. | ||
| * - Attribute absent: | ||
| * returns null. | ||
| * | ||
| * The selector wraps the value in JSON-encoded quotes so any special CSS | ||
| * characters in the value are escaped uniformly. | ||
| */ | ||
| export declare const explicitTrackingAttribute: Strategy; | ||
| //# sourceMappingURL=explicit-tracking-attribute.d.ts.map |
| {"version":3,"file":"explicit-tracking-attribute.d.ts","sourceRoot":"","sources":["../../../src/strategies/explicit-tracking-attribute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,yBAAyB,EAAE,QAavC,CAAC"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.explicitTrackingAttribute = void 0; | ||
| /** | ||
| * First strategy in the chain. Customer-controlled anchor selector via the | ||
| * configured tracking attribute (default: `data-amp-track-id`). | ||
| * | ||
| * Semantics: | ||
| * | ||
| * - Attribute set with a non-empty value: | ||
| * returns `[<attr>="<value>"]` — uses this element as an explicit anchor. | ||
| * - Attribute set with an empty value: | ||
| * returns null. The empty value is a suppression signal for downstream | ||
| * components (the `stableId` strategy and the fallback both consult | ||
| * `getStableId` which honors the empty-value semantic). This strategy | ||
| * doesn't anchor on the element either. | ||
| * - Attribute absent: | ||
| * returns null. | ||
| * | ||
| * The selector wraps the value in JSON-encoded quotes so any special CSS | ||
| * characters in the value are escaped uniformly. | ||
| */ | ||
| exports.explicitTrackingAttribute = { | ||
| name: 'explicitTrackingAttribute', | ||
| try: function (el, ctx) { | ||
| var attrName = ctx.config.explicitTrackingAttribute; | ||
| var value = el.getAttribute(attrName); | ||
| if (value === null || value === '') { | ||
| return null; | ||
| } | ||
| // JSON.stringify wraps in double quotes and escapes embedded quotes / | ||
| // backslashes — the resulting attribute selector is valid CSS. | ||
| return "[".concat(attrName, "=").concat(JSON.stringify(value), "]"); | ||
| }, | ||
| }; | ||
| //# sourceMappingURL=explicit-tracking-attribute.js.map |
| {"version":3,"file":"explicit-tracking-attribute.js","sourceRoot":"","sources":["../../../src/strategies/explicit-tracking-attribute.ts"],"names":[],"mappings":";;;AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACU,QAAA,yBAAyB,GAAa;IACjD,IAAI,EAAE,2BAA2B;IAEjC,GAAG,YAAC,EAAE,EAAE,GAAG;QACT,IAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,yBAAyB,CAAC;QACtD,IAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE;YAClC,OAAO,IAAI,CAAC;SACb;QACD,sEAAsE;QACtE,+DAA+D;QAC/D,OAAO,WAAI,QAAQ,cAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAG,CAAC;IAClD,CAAC;CACF,CAAC","sourcesContent":["import { Strategy } from '../types';\n\n/**\n * First strategy in the chain. Customer-controlled anchor selector via the\n * configured tracking attribute (default: `data-amp-track-id`).\n *\n * Semantics:\n *\n * - Attribute set with a non-empty value:\n * returns `[<attr>=\"<value>\"]` — uses this element as an explicit anchor.\n * - Attribute set with an empty value:\n * returns null. The empty value is a suppression signal for downstream\n * components (the `stableId` strategy and the fallback both consult\n * `getStableId` which honors the empty-value semantic). This strategy\n * doesn't anchor on the element either.\n * - Attribute absent:\n * returns null.\n *\n * The selector wraps the value in JSON-encoded quotes so any special CSS\n * characters in the value are escaped uniformly.\n */\nexport const explicitTrackingAttribute: Strategy = {\n name: 'explicitTrackingAttribute',\n\n try(el, ctx) {\n const attrName = ctx.config.explicitTrackingAttribute;\n const value = el.getAttribute(attrName);\n if (value === null || value === '') {\n return null;\n }\n // JSON.stringify wraps in double quotes and escapes embedded quotes /\n // backslashes — the resulting attribute selector is valid CSS.\n return `[${attrName}=${JSON.stringify(value)}]`;\n },\n};\n"]} |
| import { Strategy } from '../types'; | ||
| /** | ||
| * Second strategy in the chain. Anchors on the element's `id` when the id is | ||
| * "stable" — i.e., present, not suppressed via the explicit-tracking-attribute | ||
| * empty-value mechanism, and not matching any autogenerated-id pattern. | ||
| * | ||
| * Returns `tag#id` (CSS-escaped) when the id is usable; null otherwise. | ||
| * | ||
| * Filtering is delegated to `getStableId` so this strategy and the | ||
| * `fallback-css-path` walker (in the orchestration PR) share a single source | ||
| * of truth for "is this id usable?" | ||
| */ | ||
| export declare const stableId: Strategy; | ||
| //# sourceMappingURL=stable-id.d.ts.map |
| {"version":3,"file":"stable-id.d.ts","sourceRoot":"","sources":["../../../src/strategies/stable-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAIpC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,EAAE,QAUtB,CAAC"} |
| "use strict"; | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| exports.stableId = void 0; | ||
| var get_stable_id_1 = require("../helpers/get-stable-id"); | ||
| var escape_id_1 = require("../helpers/escape-id"); | ||
| /** | ||
| * Second strategy in the chain. Anchors on the element's `id` when the id is | ||
| * "stable" — i.e., present, not suppressed via the explicit-tracking-attribute | ||
| * empty-value mechanism, and not matching any autogenerated-id pattern. | ||
| * | ||
| * Returns `tag#id` (CSS-escaped) when the id is usable; null otherwise. | ||
| * | ||
| * Filtering is delegated to `getStableId` so this strategy and the | ||
| * `fallback-css-path` walker (in the orchestration PR) share a single source | ||
| * of truth for "is this id usable?" | ||
| */ | ||
| exports.stableId = { | ||
| name: 'stableId', | ||
| try: function (el, ctx) { | ||
| var id = (0, get_stable_id_1.getStableId)(el, ctx.config); | ||
| if (id === null) { | ||
| return null; | ||
| } | ||
| return "".concat(el.tagName.toLowerCase(), "#").concat((0, escape_id_1.escapeIdForCss)(id)); | ||
| }, | ||
| }; | ||
| //# sourceMappingURL=stable-id.js.map |
| {"version":3,"file":"stable-id.js","sourceRoot":"","sources":["../../../src/strategies/stable-id.ts"],"names":[],"mappings":";;;AACA,0DAAuD;AACvD,kDAAsD;AAEtD;;;;;;;;;;GAUG;AACU,QAAA,QAAQ,GAAa;IAChC,IAAI,EAAE,UAAU;IAEhB,GAAG,YAAC,EAAE,EAAE,GAAG;QACT,IAAM,EAAE,GAAG,IAAA,2BAAW,EAAC,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,OAAO,IAAI,CAAC;SACb;QACD,OAAO,UAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,cAAI,IAAA,0BAAc,EAAC,EAAE,CAAC,CAAE,CAAC;IAC7D,CAAC;CACF,CAAC","sourcesContent":["import { Strategy } from '../types';\nimport { getStableId } from '../helpers/get-stable-id';\nimport { escapeIdForCss } from '../helpers/escape-id';\n\n/**\n * Second strategy in the chain. Anchors on the element's `id` when the id is\n * \"stable\" — i.e., present, not suppressed via the explicit-tracking-attribute\n * empty-value mechanism, and not matching any autogenerated-id pattern.\n *\n * Returns `tag#id` (CSS-escaped) when the id is usable; null otherwise.\n *\n * Filtering is delegated to `getStableId` so this strategy and the\n * `fallback-css-path` walker (in the orchestration PR) share a single source\n * of truth for \"is this id usable?\"\n */\nexport const stableId: Strategy = {\n name: 'stableId',\n\n try(el, ctx) {\n const id = getStableId(el, ctx.config);\n if (id === null) {\n return null;\n }\n return `${el.tagName.toLowerCase()}#${escapeIdForCss(id)}`;\n },\n};\n"]} |
| /** | ||
| * Core type definitions for the v1 element-selector algorithm. | ||
| * | ||
| * These are the public-API contract that consumers (autocapture SDK, dashboard | ||
| * tagging UI, Chrome extension visual tagger) design against. They land before | ||
| * any implementation so the contract is stable for cross-team review. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| /** | ||
| * A pluggable selector strategy. Given an element + context, returns a CSS | ||
| * selector string candidate (which the orchestrator will then uniqueness-check) | ||
| * or `null` when the strategy doesn't apply to this element. | ||
| */ | ||
| export interface Strategy { | ||
| /** Stable identifier for the strategy. Used for diagnostics and remote-config references. */ | ||
| readonly name: string; | ||
| /** | ||
| * Produce a candidate selector for `el`, or `null` if this strategy has | ||
| * nothing to contribute for this element. Strategies are pure transforms — | ||
| * they never call `querySelectorAll` themselves; uniqueness is checked by | ||
| * the orchestrator in the next layer up. | ||
| */ | ||
| try(el: Element, ctx: StrategyContext): string | null; | ||
| } | ||
| /** | ||
| * Context passed to every strategy invocation. `scope` is the document or | ||
| * shadow root the orchestrator will use for uniqueness checks; `config` is the | ||
| * resolved configuration (defaults + remote + local options merged). | ||
| */ | ||
| export interface StrategyContext { | ||
| scope: ParentNode; | ||
| config: ResolvedSelectorConfig; | ||
| } | ||
| /** | ||
| * Logger shape accepted by element-selector diagnostics. | ||
| * | ||
| * This is intentionally structural so callers can pass the SDK's | ||
| * `@amplitude/analytics-core` ILogger without this package needing to import | ||
| * analytics-core types during standalone tests/builds. | ||
| */ | ||
| export interface ElementSelectorLogger { | ||
| warn(...args: unknown[]): void; | ||
| debug(...args: unknown[]): void; | ||
| } | ||
| /** | ||
| * Fully-resolved configuration consumed at runtime. Produced by | ||
| * `resolveSelectorConfig` (lands in the orchestration PR) from the | ||
| * `ElementSelectorRemoteConfig` payload plus built-in defaults. | ||
| */ | ||
| export interface ResolvedSelectorConfig { | ||
| /** Master kill switch. When false, autocapture reverts to legacy cssPath. */ | ||
| enabled: boolean; | ||
| /** Attribute name customers can use to set or suppress anchors. Default: 'data-amp-track-id'. */ | ||
| explicitTrackingAttribute: string; | ||
| /** Compiled regex patterns matching ids the algorithm should treat as autogenerated. */ | ||
| autogeneratedIdPatterns: RegExp[]; | ||
| /** Compiled regex patterns matching classes the fallback should filter from sibling disambiguation. */ | ||
| unstableClassPatterns: RegExp[]; | ||
| /** Optional throttle on the ancestor walk. When undefined, the walk runs to <html>. */ | ||
| maxAncestorWalkDepth?: number; | ||
| } | ||
| /** | ||
| * Remote-config payload shape. Each field is optional; absent fields fall back | ||
| * to built-in defaults. Customers see the defaults in the remote-config UI and | ||
| * may add or remove patterns as needed. | ||
| */ | ||
| export interface ElementSelectorRemoteConfig { | ||
| /** Kill switch. When set, overrides the default at resolve time. */ | ||
| enabled?: boolean; | ||
| /** Override the attribute name used for explicit tracking. */ | ||
| explicitTrackingAttribute?: string; | ||
| /** Full replacement for the autogenerated-id pattern list. When omitted, defaults apply. */ | ||
| autogeneratedIdPatterns?: string[]; | ||
| /** Full replacement for the unstable-class pattern list. When omitted, defaults apply. */ | ||
| unstableClassPatterns?: string[]; | ||
| /** Defensive throttle on the ancestor walk. Omit for unbounded walking to <html>. */ | ||
| maxAncestorWalkDepth?: number; | ||
| } | ||
| /** | ||
| * Public interface of the runtime engine. Implementation lands in the | ||
| * orchestration PR; the interface is declared here so consumers can write | ||
| * against the contract before the class exists. | ||
| */ | ||
| export interface SelectorEngine { | ||
| /** Produce a CSS selector string identifying `el` using the engine's current config. */ | ||
| generate(el: Element): string; | ||
| /** Read-only access to the current resolved config — for diagnostics and consumer hydration. */ | ||
| getConfig(): Readonly<ResolvedSelectorConfig>; | ||
| /** Replace the current config. Notifies any onConfigChange subscribers. */ | ||
| updateConfig(next: ResolvedSelectorConfig): void; | ||
| /** | ||
| * Subscribe to config-change notifications. Returns an unsubscribe function. | ||
| * Used by the Chrome extension to keep its locally-hydrated engine in sync | ||
| * with the customer's SDK as remote config updates flow in. | ||
| */ | ||
| onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void; | ||
| } | ||
| //# sourceMappingURL=types.d.ts.map |
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAAC;CACvD;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;IACjB,iGAAiG;IACjG,yBAAyB,EAAE,MAAM,CAAC;IAClC,wFAAwF;IACxF,uBAAuB,EAAE,MAAM,EAAE,CAAC;IAClC,uGAAuG;IACvG,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,uFAAuF;IACvF,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8DAA8D;IAC9D,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,4FAA4F;IAC5F,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,0FAA0F;IAC1F,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,qFAAqF;IACrF,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,wFAAwF;IACxF,QAAQ,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,CAAC;IAE9B,gGAAgG;IAChG,SAAS,IAAI,QAAQ,CAAC,sBAAsB,CAAC,CAAC;IAE9C,2EAA2E;IAC3E,YAAY,CAAC,IAAI,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAEjD;;;;OAIG;IACH,cAAc,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC1E"} |
| "use strict"; | ||
| /** | ||
| * Core type definitions for the v1 element-selector algorithm. | ||
| * | ||
| * These are the public-API contract that consumers (autocapture SDK, dashboard | ||
| * tagging UI, Chrome extension visual tagger) design against. They land before | ||
| * any implementation so the contract is stable for cross-team review. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| Object.defineProperty(exports, "__esModule", { value: true }); | ||
| //# sourceMappingURL=types.js.map |
| {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":";AAAA;;;;;;;;;GASG","sourcesContent":["/**\n * Core type definitions for the v1 element-selector algorithm.\n *\n * These are the public-API contract that consumers (autocapture SDK, dashboard\n * tagging UI, Chrome extension visual tagger) design against. They land before\n * any implementation so the contract is stable for cross-team review.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\n/**\n * A pluggable selector strategy. Given an element + context, returns a CSS\n * selector string candidate (which the orchestrator will then uniqueness-check)\n * or `null` when the strategy doesn't apply to this element.\n */\nexport interface Strategy {\n /** Stable identifier for the strategy. Used for diagnostics and remote-config references. */\n readonly name: string;\n\n /**\n * Produce a candidate selector for `el`, or `null` if this strategy has\n * nothing to contribute for this element. Strategies are pure transforms —\n * they never call `querySelectorAll` themselves; uniqueness is checked by\n * the orchestrator in the next layer up.\n */\n try(el: Element, ctx: StrategyContext): string | null;\n}\n\n/**\n * Context passed to every strategy invocation. `scope` is the document or\n * shadow root the orchestrator will use for uniqueness checks; `config` is the\n * resolved configuration (defaults + remote + local options merged).\n */\nexport interface StrategyContext {\n scope: ParentNode;\n config: ResolvedSelectorConfig;\n}\n\n/**\n * Logger shape accepted by element-selector diagnostics.\n *\n * This is intentionally structural so callers can pass the SDK's\n * `@amplitude/analytics-core` ILogger without this package needing to import\n * analytics-core types during standalone tests/builds.\n */\nexport interface ElementSelectorLogger {\n warn(...args: unknown[]): void;\n debug(...args: unknown[]): void;\n}\n\n/**\n * Fully-resolved configuration consumed at runtime. Produced by\n * `resolveSelectorConfig` (lands in the orchestration PR) from the\n * `ElementSelectorRemoteConfig` payload plus built-in defaults.\n */\nexport interface ResolvedSelectorConfig {\n /** Master kill switch. When false, autocapture reverts to legacy cssPath. */\n enabled: boolean;\n /** Attribute name customers can use to set or suppress anchors. Default: 'data-amp-track-id'. */\n explicitTrackingAttribute: string;\n /** Compiled regex patterns matching ids the algorithm should treat as autogenerated. */\n autogeneratedIdPatterns: RegExp[];\n /** Compiled regex patterns matching classes the fallback should filter from sibling disambiguation. */\n unstableClassPatterns: RegExp[];\n /** Optional throttle on the ancestor walk. When undefined, the walk runs to <html>. */\n maxAncestorWalkDepth?: number;\n}\n\n/**\n * Remote-config payload shape. Each field is optional; absent fields fall back\n * to built-in defaults. Customers see the defaults in the remote-config UI and\n * may add or remove patterns as needed.\n */\nexport interface ElementSelectorRemoteConfig {\n /** Kill switch. When set, overrides the default at resolve time. */\n enabled?: boolean;\n /** Override the attribute name used for explicit tracking. */\n explicitTrackingAttribute?: string;\n /** Full replacement for the autogenerated-id pattern list. When omitted, defaults apply. */\n autogeneratedIdPatterns?: string[];\n /** Full replacement for the unstable-class pattern list. When omitted, defaults apply. */\n unstableClassPatterns?: string[];\n /** Defensive throttle on the ancestor walk. Omit for unbounded walking to <html>. */\n maxAncestorWalkDepth?: number;\n}\n\n/**\n * Public interface of the runtime engine. Implementation lands in the\n * orchestration PR; the interface is declared here so consumers can write\n * against the contract before the class exists.\n */\nexport interface SelectorEngine {\n /** Produce a CSS selector string identifying `el` using the engine's current config. */\n generate(el: Element): string;\n\n /** Read-only access to the current resolved config — for diagnostics and consumer hydration. */\n getConfig(): Readonly<ResolvedSelectorConfig>;\n\n /** Replace the current config. Notifies any onConfigChange subscribers. */\n updateConfig(next: ResolvedSelectorConfig): void;\n\n /**\n * Subscribe to config-change notifications. Returns an unsubscribe function.\n * Used by the Chrome extension to keep its locally-hydrated engine in sync\n * with the customer's SDK as remote config updates flow in.\n */\n onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void;\n}\n"]} |
| /** | ||
| * Config resolver — collapses the optional, string-based remote-config payload | ||
| * into the fully-typed `ResolvedSelectorConfig` that strategies, orchestrator, | ||
| * and fallback consume at runtime. | ||
| * | ||
| * Responsibilities: | ||
| * | ||
| * 1. Apply defaults for every field. The defaults (enabled = false, | ||
| * attribute = `data-amp-track-id`, built-in pattern packs) match the | ||
| * design doc's "v1 defaults" table. | ||
| * | ||
| * 2. Compile regex strings into `RegExp[]`. Bad regexes are skipped by the | ||
| * underlying `compile` helpers; when a logger is provided it surfaces a | ||
| * warning for each invalid pattern so customers can diagnose why their | ||
| * override isn't taking effect. | ||
| * | ||
| * 3. Treat customer-provided pattern lists as a **full replacement** of the | ||
| * defaults — not a merge. This matches the design doc: "The customer | ||
| * sees the defaults in the remote-config UI and may add or remove | ||
| * patterns as needed." If a customer provides `autogeneratedIdPatterns: | ||
| * []`, that's their explicit request to disable autogen filtering. | ||
| * | ||
| * 4. Clamp `maxAncestorWalkDepth` into a sane range. Negative / zero values | ||
| * are coerced to `undefined` (treat as "no limit") rather than throwing — | ||
| * remote config should never crash the engine. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { ElementSelectorLogger, ElementSelectorRemoteConfig, ResolvedSelectorConfig } from '../types'; | ||
| /** | ||
| * Built-in defaults applied when a field is absent from the remote payload. | ||
| * Exported so consumers (and tests) can introspect the baseline without | ||
| * round-tripping through `resolveSelectorConfig({})`. | ||
| */ | ||
| export declare const DEFAULT_RESOLVED_CONFIG: Readonly<ResolvedSelectorConfig>; | ||
| /** | ||
| * Resolve a (possibly partial, possibly absent) remote-config payload into a | ||
| * fully-typed runtime config. Always returns a fresh object; never mutates the | ||
| * input. | ||
| * | ||
| * Field-by-field semantics: | ||
| * | ||
| * - `enabled`: defaults to false (the engine ships dormant). Any boolean in | ||
| * the payload — including `false` — wins. | ||
| * - `explicitTrackingAttribute`: defaults to `data-amp-track-id`. Empty | ||
| * strings are rejected (they'd make the strategy match every element); | ||
| * fall back to the default in that case. | ||
| * - `autogeneratedIdPatterns` / `unstableClassPatterns`: present → compile | ||
| * the strings (skipping invalid regexes), use the compiled list as-is. | ||
| * Absent → use the defaults. | ||
| * - `maxAncestorWalkDepth`: positive finite integer → use as-is. Anything | ||
| * else (zero, negative, NaN, Infinity) → `undefined` (no limit). | ||
| */ | ||
| export declare function resolveSelectorConfig(remote?: ElementSelectorRemoteConfig, logger?: ElementSelectorLogger): ResolvedSelectorConfig; | ||
| //# sourceMappingURL=resolve-config.d.ts.map |
| {"version":3,"file":"resolve-config.d.ts","sourceRoot":"","sources":["../../../src/config/resolve-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,OAAO,EAAE,qBAAqB,EAAE,2BAA2B,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAOtG;;;;GAIG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,sBAAsB,CAQnE,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,CAAC,EAAE,2BAA2B,EACpC,MAAM,CAAC,EAAE,qBAAqB,GAC7B,sBAAsB,CA4BxB"} |
| /** | ||
| * Config resolver — collapses the optional, string-based remote-config payload | ||
| * into the fully-typed `ResolvedSelectorConfig` that strategies, orchestrator, | ||
| * and fallback consume at runtime. | ||
| * | ||
| * Responsibilities: | ||
| * | ||
| * 1. Apply defaults for every field. The defaults (enabled = false, | ||
| * attribute = `data-amp-track-id`, built-in pattern packs) match the | ||
| * design doc's "v1 defaults" table. | ||
| * | ||
| * 2. Compile regex strings into `RegExp[]`. Bad regexes are skipped by the | ||
| * underlying `compile` helpers; when a logger is provided it surfaces a | ||
| * warning for each invalid pattern so customers can diagnose why their | ||
| * override isn't taking effect. | ||
| * | ||
| * 3. Treat customer-provided pattern lists as a **full replacement** of the | ||
| * defaults — not a merge. This matches the design doc: "The customer | ||
| * sees the defaults in the remote-config UI and may add or remove | ||
| * patterns as needed." If a customer provides `autogeneratedIdPatterns: | ||
| * []`, that's their explicit request to disable autogen filtering. | ||
| * | ||
| * 4. Clamp `maxAncestorWalkDepth` into a sane range. Negative / zero values | ||
| * are coerced to `undefined` (treat as "no limit") rather than throwing — | ||
| * remote config should never crash the engine. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { __read, __spreadArray } from "tslib"; | ||
| import { DEFAULT_AUTOGENERATED_ID_PATTERNS, compile as compileAutogeneratedIdPatterns, } from '../patterns/autogenerated-ids'; | ||
| import { DEFAULT_UNSTABLE_CLASS_PATTERNS, compile as compileUnstableClassPatterns } from '../patterns/unstable-classes'; | ||
| /** | ||
| * Built-in defaults applied when a field is absent from the remote payload. | ||
| * Exported so consumers (and tests) can introspect the baseline without | ||
| * round-tripping through `resolveSelectorConfig({})`. | ||
| */ | ||
| export var DEFAULT_RESOLVED_CONFIG = Object.freeze({ | ||
| // v1 ships disabled — autocapture only flips to the new engine when the | ||
| // remote config explicitly enables it for an org. | ||
| enabled: false, | ||
| explicitTrackingAttribute: 'data-amp-track-id', | ||
| autogeneratedIdPatterns: __spreadArray([], __read(DEFAULT_AUTOGENERATED_ID_PATTERNS), false), | ||
| unstableClassPatterns: __spreadArray([], __read(DEFAULT_UNSTABLE_CLASS_PATTERNS), false), | ||
| maxAncestorWalkDepth: undefined, | ||
| }); | ||
| /** | ||
| * Resolve a (possibly partial, possibly absent) remote-config payload into a | ||
| * fully-typed runtime config. Always returns a fresh object; never mutates the | ||
| * input. | ||
| * | ||
| * Field-by-field semantics: | ||
| * | ||
| * - `enabled`: defaults to false (the engine ships dormant). Any boolean in | ||
| * the payload — including `false` — wins. | ||
| * - `explicitTrackingAttribute`: defaults to `data-amp-track-id`. Empty | ||
| * strings are rejected (they'd make the strategy match every element); | ||
| * fall back to the default in that case. | ||
| * - `autogeneratedIdPatterns` / `unstableClassPatterns`: present → compile | ||
| * the strings (skipping invalid regexes), use the compiled list as-is. | ||
| * Absent → use the defaults. | ||
| * - `maxAncestorWalkDepth`: positive finite integer → use as-is. Anything | ||
| * else (zero, negative, NaN, Infinity) → `undefined` (no limit). | ||
| */ | ||
| export function resolveSelectorConfig(remote, logger) { | ||
| if (!remote) { | ||
| // Return a fresh copy so callers can safely mutate the result without | ||
| // poisoning the frozen defaults singleton. | ||
| return cloneDefaults(); | ||
| } | ||
| var resolved = cloneDefaults(); | ||
| if (typeof remote.enabled === 'boolean') { | ||
| resolved.enabled = remote.enabled; | ||
| } | ||
| if (typeof remote.explicitTrackingAttribute === 'string' && remote.explicitTrackingAttribute !== '') { | ||
| resolved.explicitTrackingAttribute = remote.explicitTrackingAttribute; | ||
| } | ||
| if (Array.isArray(remote.autogeneratedIdPatterns)) { | ||
| resolved.autogeneratedIdPatterns = compileAutogeneratedIdPatterns(remote.autogeneratedIdPatterns, logger); | ||
| } | ||
| if (Array.isArray(remote.unstableClassPatterns)) { | ||
| resolved.unstableClassPatterns = compileUnstableClassPatterns(remote.unstableClassPatterns, logger); | ||
| } | ||
| resolved.maxAncestorWalkDepth = clampDepth(remote.maxAncestorWalkDepth); | ||
| return resolved; | ||
| } | ||
| /** | ||
| * Coerce a remote-supplied depth value into the runtime contract: | ||
| * - undefined / non-number / NaN / Infinity / <= 0 → undefined (no limit) | ||
| * - positive finite number → floor()'d integer | ||
| */ | ||
| function clampDepth(depth) { | ||
| if (typeof depth !== 'number') | ||
| return undefined; | ||
| if (!Number.isFinite(depth)) | ||
| return undefined; | ||
| if (depth <= 0) | ||
| return undefined; | ||
| return Math.floor(depth); | ||
| } | ||
| function cloneDefaults() { | ||
| return { | ||
| enabled: DEFAULT_RESOLVED_CONFIG.enabled, | ||
| explicitTrackingAttribute: DEFAULT_RESOLVED_CONFIG.explicitTrackingAttribute, | ||
| autogeneratedIdPatterns: __spreadArray([], __read(DEFAULT_RESOLVED_CONFIG.autogeneratedIdPatterns), false), | ||
| unstableClassPatterns: __spreadArray([], __read(DEFAULT_RESOLVED_CONFIG.unstableClassPatterns), false), | ||
| maxAncestorWalkDepth: DEFAULT_RESOLVED_CONFIG.maxAncestorWalkDepth, | ||
| }; | ||
| } | ||
| //# sourceMappingURL=resolve-config.js.map |
| {"version":3,"file":"resolve-config.js","sourceRoot":"","sources":["../../../src/config/resolve-config.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;;AAGH,OAAO,EACL,iCAAiC,EACjC,OAAO,IAAI,8BAA8B,GAC1C,MAAM,+BAA+B,CAAC;AACvC,OAAO,EAAE,+BAA+B,EAAE,OAAO,IAAI,4BAA4B,EAAE,MAAM,8BAA8B,CAAC;AAExH;;;;GAIG;AACH,MAAM,CAAC,IAAM,uBAAuB,GAAqC,MAAM,CAAC,MAAM,CAAC;IACrF,wEAAwE;IACxE,kDAAkD;IAClD,OAAO,EAAE,KAAK;IACd,yBAAyB,EAAE,mBAAmB;IAC9C,uBAAuB,2BAAM,iCAAiC,SAAC;IAC/D,qBAAqB,2BAAM,+BAA+B,SAAC;IAC3D,oBAAoB,EAAE,SAAS;CAChC,CAAC,CAAC;AAEH;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,UAAU,qBAAqB,CACnC,MAAoC,EACpC,MAA8B;IAE9B,IAAI,CAAC,MAAM,EAAE;QACX,sEAAsE;QACtE,2CAA2C;QAC3C,OAAO,aAAa,EAAE,CAAC;KACxB;IAED,IAAM,QAAQ,GAA2B,aAAa,EAAE,CAAC;IAEzD,IAAI,OAAO,MAAM,CAAC,OAAO,KAAK,SAAS,EAAE;QACvC,QAAQ,CAAC,OAAO,GAAG,MAAM,CAAC,OAAO,CAAC;KACnC;IAED,IAAI,OAAO,MAAM,CAAC,yBAAyB,KAAK,QAAQ,IAAI,MAAM,CAAC,yBAAyB,KAAK,EAAE,EAAE;QACnG,QAAQ,CAAC,yBAAyB,GAAG,MAAM,CAAC,yBAAyB,CAAC;KACvE;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,uBAAuB,CAAC,EAAE;QACjD,QAAQ,CAAC,uBAAuB,GAAG,8BAA8B,CAAC,MAAM,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;KAC3G;IAED,IAAI,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,qBAAqB,CAAC,EAAE;QAC/C,QAAQ,CAAC,qBAAqB,GAAG,4BAA4B,CAAC,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;KACrG;IAED,QAAQ,CAAC,oBAAoB,GAAG,UAAU,CAAC,MAAM,CAAC,oBAAoB,CAAC,CAAC;IAExE,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;GAIG;AACH,SAAS,UAAU,CAAC,KAAyB;IAC3C,IAAI,OAAO,KAAK,KAAK,QAAQ;QAAE,OAAO,SAAS,CAAC;IAChD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC;QAAE,OAAO,SAAS,CAAC;IAC9C,IAAI,KAAK,IAAI,CAAC;QAAE,OAAO,SAAS,CAAC;IACjC,OAAO,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAC3B,CAAC;AAED,SAAS,aAAa;IACpB,OAAO;QACL,OAAO,EAAE,uBAAuB,CAAC,OAAO;QACxC,yBAAyB,EAAE,uBAAuB,CAAC,yBAAyB;QAC5E,uBAAuB,2BAAM,uBAAuB,CAAC,uBAAuB,SAAC;QAC7E,qBAAqB,2BAAM,uBAAuB,CAAC,qBAAqB,SAAC;QACzE,oBAAoB,EAAE,uBAAuB,CAAC,oBAAoB;KACnE,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Config resolver — collapses the optional, string-based remote-config payload\n * into the fully-typed `ResolvedSelectorConfig` that strategies, orchestrator,\n * and fallback consume at runtime.\n *\n * Responsibilities:\n *\n * 1. Apply defaults for every field. The defaults (enabled = false,\n * attribute = `data-amp-track-id`, built-in pattern packs) match the\n * design doc's \"v1 defaults\" table.\n *\n * 2. Compile regex strings into `RegExp[]`. Bad regexes are skipped by the\n * underlying `compile` helpers; when a logger is provided it surfaces a\n * warning for each invalid pattern so customers can diagnose why their\n * override isn't taking effect.\n *\n * 3. Treat customer-provided pattern lists as a **full replacement** of the\n * defaults — not a merge. This matches the design doc: \"The customer\n * sees the defaults in the remote-config UI and may add or remove\n * patterns as needed.\" If a customer provides `autogeneratedIdPatterns:\n * []`, that's their explicit request to disable autogen filtering.\n *\n * 4. Clamp `maxAncestorWalkDepth` into a sane range. Negative / zero values\n * are coerced to `undefined` (treat as \"no limit\") rather than throwing —\n * remote config should never crash the engine.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ElementSelectorRemoteConfig, ResolvedSelectorConfig } from '../types';\nimport {\n DEFAULT_AUTOGENERATED_ID_PATTERNS,\n compile as compileAutogeneratedIdPatterns,\n} from '../patterns/autogenerated-ids';\nimport { DEFAULT_UNSTABLE_CLASS_PATTERNS, compile as compileUnstableClassPatterns } from '../patterns/unstable-classes';\n\n/**\n * Built-in defaults applied when a field is absent from the remote payload.\n * Exported so consumers (and tests) can introspect the baseline without\n * round-tripping through `resolveSelectorConfig({})`.\n */\nexport const DEFAULT_RESOLVED_CONFIG: Readonly<ResolvedSelectorConfig> = Object.freeze({\n // v1 ships disabled — autocapture only flips to the new engine when the\n // remote config explicitly enables it for an org.\n enabled: false,\n explicitTrackingAttribute: 'data-amp-track-id',\n autogeneratedIdPatterns: [...DEFAULT_AUTOGENERATED_ID_PATTERNS],\n unstableClassPatterns: [...DEFAULT_UNSTABLE_CLASS_PATTERNS],\n maxAncestorWalkDepth: undefined,\n});\n\n/**\n * Resolve a (possibly partial, possibly absent) remote-config payload into a\n * fully-typed runtime config. Always returns a fresh object; never mutates the\n * input.\n *\n * Field-by-field semantics:\n *\n * - `enabled`: defaults to false (the engine ships dormant). Any boolean in\n * the payload — including `false` — wins.\n * - `explicitTrackingAttribute`: defaults to `data-amp-track-id`. Empty\n * strings are rejected (they'd make the strategy match every element);\n * fall back to the default in that case.\n * - `autogeneratedIdPatterns` / `unstableClassPatterns`: present → compile\n * the strings (skipping invalid regexes), use the compiled list as-is.\n * Absent → use the defaults.\n * - `maxAncestorWalkDepth`: positive finite integer → use as-is. Anything\n * else (zero, negative, NaN, Infinity) → `undefined` (no limit).\n */\nexport function resolveSelectorConfig(\n remote?: ElementSelectorRemoteConfig,\n logger?: ElementSelectorLogger,\n): ResolvedSelectorConfig {\n if (!remote) {\n // Return a fresh copy so callers can safely mutate the result without\n // poisoning the frozen defaults singleton.\n return cloneDefaults();\n }\n\n const resolved: ResolvedSelectorConfig = cloneDefaults();\n\n if (typeof remote.enabled === 'boolean') {\n resolved.enabled = remote.enabled;\n }\n\n if (typeof remote.explicitTrackingAttribute === 'string' && remote.explicitTrackingAttribute !== '') {\n resolved.explicitTrackingAttribute = remote.explicitTrackingAttribute;\n }\n\n if (Array.isArray(remote.autogeneratedIdPatterns)) {\n resolved.autogeneratedIdPatterns = compileAutogeneratedIdPatterns(remote.autogeneratedIdPatterns, logger);\n }\n\n if (Array.isArray(remote.unstableClassPatterns)) {\n resolved.unstableClassPatterns = compileUnstableClassPatterns(remote.unstableClassPatterns, logger);\n }\n\n resolved.maxAncestorWalkDepth = clampDepth(remote.maxAncestorWalkDepth);\n\n return resolved;\n}\n\n/**\n * Coerce a remote-supplied depth value into the runtime contract:\n * - undefined / non-number / NaN / Infinity / <= 0 → undefined (no limit)\n * - positive finite number → floor()'d integer\n */\nfunction clampDepth(depth: number | undefined): number | undefined {\n if (typeof depth !== 'number') return undefined;\n if (!Number.isFinite(depth)) return undefined;\n if (depth <= 0) return undefined;\n return Math.floor(depth);\n}\n\nfunction cloneDefaults(): ResolvedSelectorConfig {\n return {\n enabled: DEFAULT_RESOLVED_CONFIG.enabled,\n explicitTrackingAttribute: DEFAULT_RESOLVED_CONFIG.explicitTrackingAttribute,\n autogeneratedIdPatterns: [...DEFAULT_RESOLVED_CONFIG.autogeneratedIdPatterns],\n unstableClassPatterns: [...DEFAULT_RESOLVED_CONFIG.unstableClassPatterns],\n maxAncestorWalkDepth: DEFAULT_RESOLVED_CONFIG.maxAncestorWalkDepth,\n };\n}\n"]} |
| /** | ||
| * Engine factory — composes the strategy chain (via `runOrchestrator`) with | ||
| * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine` | ||
| * interface. | ||
| * | ||
| * This is the surface every consumer talks to: | ||
| * | ||
| * - autocapture SDK plugin → instantiates one engine per init() call, | ||
| * wires it into the click handler, and forwards remote-config updates | ||
| * via `updateConfig`. | ||
| * - app.amplitude.com tagging UI → fetches the customer's remote config | ||
| * out-of-band and stands up a transient engine to compute selectors for | ||
| * elements the user clicks in the iframe. | ||
| * - Chrome extension visual tagger → reads the customer's already-live | ||
| * engine off `window.amplitude.elementSelector` (when present) and | ||
| * subscribes to `onConfigChange` so the extension's preview stays in | ||
| * sync with the customer's runtime config. | ||
| * | ||
| * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather | ||
| * than the raw remote payload — config resolution is a separate concern handled | ||
| * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets | ||
| * dashboard / extension consumers stand up an engine from a static snapshot | ||
| * without re-running the full resolver. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { ElementSelectorLogger, ResolvedSelectorConfig, SelectorEngine } from './types'; | ||
| import { OrchestratorOptions } from './orchestrator'; | ||
| export interface CreateSelectorEngineOptions { | ||
| /** Optional document or shadow root for uniqueness checks. Defaults per-call to the target's owner document. */ | ||
| scope?: ParentNode; | ||
| /** Optional override of the strategy chain. Primarily for testing / dashboard ad-hoc runs. */ | ||
| strategies?: OrchestratorOptions['strategies']; | ||
| /** | ||
| * Optional logger threaded through the orchestrator and the subscriber-fan-out | ||
| * inside `updateConfig`. When provided, the engine surfaces malformed | ||
| * selectors (`debug`) and listener exceptions (`warn`); when absent it stays | ||
| * silent — preserving the legacy "fire-and-forget" semantics existing | ||
| * consumers may rely on. | ||
| */ | ||
| logger?: ElementSelectorLogger; | ||
| } | ||
| /** | ||
| * Build a `SelectorEngine` bound to the supplied config. | ||
| * | ||
| * The returned engine is independent — calling the factory twice yields two | ||
| * engines with separate config state and separate subscriber lists. This is the | ||
| * shape the autocapture plugin wants (one engine per SDK instance) and the | ||
| * shape the Chrome extension consumes off the page (the extension reads, never | ||
| * writes). | ||
| */ | ||
| export declare function createSelectorEngine(initialConfig: ResolvedSelectorConfig, options?: CreateSelectorEngineOptions): SelectorEngine; | ||
| //# sourceMappingURL=engine.d.ts.map |
| {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AACxF,OAAO,EAAmB,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAGtE,MAAM,WAAW,2BAA2B;IAC1C,gHAAgH;IAChH,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,8FAA8F;IAC9F,UAAU,CAAC,EAAE,mBAAmB,CAAC,YAAY,CAAC,CAAC;IAC/C;;;;;;OAMG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAClC,aAAa,EAAE,sBAAsB,EACrC,OAAO,GAAE,2BAAgC,GACxC,cAAc,CAkDhB"} |
| /** | ||
| * Engine factory — composes the strategy chain (via `runOrchestrator`) with | ||
| * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine` | ||
| * interface. | ||
| * | ||
| * This is the surface every consumer talks to: | ||
| * | ||
| * - autocapture SDK plugin → instantiates one engine per init() call, | ||
| * wires it into the click handler, and forwards remote-config updates | ||
| * via `updateConfig`. | ||
| * - app.amplitude.com tagging UI → fetches the customer's remote config | ||
| * out-of-band and stands up a transient engine to compute selectors for | ||
| * elements the user clicks in the iframe. | ||
| * - Chrome extension visual tagger → reads the customer's already-live | ||
| * engine off `window.amplitude.elementSelector` (when present) and | ||
| * subscribes to `onConfigChange` so the extension's preview stays in | ||
| * sync with the customer's runtime config. | ||
| * | ||
| * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather | ||
| * than the raw remote payload — config resolution is a separate concern handled | ||
| * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets | ||
| * dashboard / extension consumers stand up an engine from a static snapshot | ||
| * without re-running the full resolver. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { __values } from "tslib"; | ||
| import { runOrchestrator } from './orchestrator'; | ||
| import { fallbackCssPath } from './fallback-css-path'; | ||
| /** | ||
| * Build a `SelectorEngine` bound to the supplied config. | ||
| * | ||
| * The returned engine is independent — calling the factory twice yields two | ||
| * engines with separate config state and separate subscriber lists. This is the | ||
| * shape the autocapture plugin wants (one engine per SDK instance) and the | ||
| * shape the Chrome extension consumes off the page (the extension reads, never | ||
| * writes). | ||
| */ | ||
| export function createSelectorEngine(initialConfig, options) { | ||
| if (options === void 0) { options = {}; } | ||
| var config = initialConfig; | ||
| var subscribers = new Set(); | ||
| var logger = options.logger; | ||
| var orchestratorOptions = { | ||
| strategies: options.strategies, | ||
| scope: options.scope, | ||
| logger: logger, | ||
| }; | ||
| return { | ||
| generate: function (el) { | ||
| var _a, _b; | ||
| // Try the strategy chain first. | ||
| var composed = runOrchestrator(el, config, orchestratorOptions); | ||
| if (composed !== null) { | ||
| return composed; | ||
| } | ||
| // Strategy chain found nothing usable — fall back to the hardened | ||
| // positional walker with the same scope used by the orchestrator. | ||
| return fallbackCssPath(el, config, { scope: (_b = (_a = options.scope) !== null && _a !== void 0 ? _a : el.ownerDocument) !== null && _b !== void 0 ? _b : document }); | ||
| }, | ||
| getConfig: function () { | ||
| return config; | ||
| }, | ||
| updateConfig: function (next) { | ||
| var e_1, _a; | ||
| config = next; | ||
| try { | ||
| // Notify subscribers in insertion order. Errors thrown by individual | ||
| // subscribers are isolated so one bad listener can't break the others — | ||
| // the extension's listener is the canonical consumer here and we don't | ||
| // want SDK-side changes to cascade-fail because of an extension bug. | ||
| for (var subscribers_1 = __values(subscribers), subscribers_1_1 = subscribers_1.next(); !subscribers_1_1.done; subscribers_1_1 = subscribers_1.next()) { | ||
| var cb = subscribers_1_1.value; | ||
| try { | ||
| cb(next); | ||
| } | ||
| catch (e) { | ||
| var message = e instanceof Error ? e.message : String(e); | ||
| logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: onConfigChange subscriber threw \u2014 ".concat(message)); | ||
| } | ||
| } | ||
| } | ||
| catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
| finally { | ||
| try { | ||
| if (subscribers_1_1 && !subscribers_1_1.done && (_a = subscribers_1.return)) _a.call(subscribers_1); | ||
| } | ||
| finally { if (e_1) throw e_1.error; } | ||
| } | ||
| }, | ||
| onConfigChange: function (cb) { | ||
| subscribers.add(cb); | ||
| return function () { | ||
| subscribers.delete(cb); | ||
| }; | ||
| }, | ||
| }; | ||
| } | ||
| //# sourceMappingURL=engine.js.map |
| {"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;;AAGH,OAAO,EAAE,eAAe,EAAuB,MAAM,gBAAgB,CAAC;AACtE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAiBtD;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAClC,aAAqC,EACrC,OAAyC;IAAzC,wBAAA,EAAA,YAAyC;IAEzC,IAAI,MAAM,GAA2B,aAAa,CAAC;IACnD,IAAM,WAAW,GAAG,IAAI,GAAG,EAA4C,CAAC;IACxE,IAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;IAE9B,IAAM,mBAAmB,GAAwB;QAC/C,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,QAAA;KACP,CAAC;IAEF,OAAO;QACL,QAAQ,YAAC,EAAW;;YAClB,gCAAgC;YAChC,IAAM,QAAQ,GAAG,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,mBAAmB,CAAC,CAAC;YAClE,IAAI,QAAQ,KAAK,IAAI,EAAE;gBACrB,OAAO,QAAQ,CAAC;aACjB;YACD,kEAAkE;YAClE,kEAAkE;YAClE,OAAO,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,EAAE,KAAK,EAAE,MAAA,MAAA,OAAO,CAAC,KAAK,mCAAI,EAAE,CAAC,aAAa,mCAAI,QAAQ,EAAE,CAAC,CAAC;QAC/F,CAAC;QAED,SAAS;YACP,OAAO,MAAM,CAAC;QAChB,CAAC;QAED,YAAY,YAAC,IAA4B;;YACvC,MAAM,GAAG,IAAI,CAAC;;gBACd,qEAAqE;gBACrE,wEAAwE;gBACxE,uEAAuE;gBACvE,qEAAqE;gBACrE,KAAiB,IAAA,gBAAA,SAAA,WAAW,CAAA,wCAAA,iEAAE;oBAAzB,IAAM,EAAE,wBAAA;oBACX,IAAI;wBACF,EAAE,CAAC,IAAI,CAAC,CAAC;qBACV;oBAAC,OAAO,CAAC,EAAE;wBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;wBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,8EAAkE,OAAO,CAAE,CAAC,CAAC;qBAC3F;iBACF;;;;;;;;;QACH,CAAC;QAED,cAAc,YAAC,EAA4C;YACzD,WAAW,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;YACpB,OAAO;gBACL,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;YACzB,CAAC,CAAC;QACJ,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["/**\n * Engine factory — composes the strategy chain (via `runOrchestrator`) with\n * the safety-net fallback (`fallbackCssPath`) behind a single `SelectorEngine`\n * interface.\n *\n * This is the surface every consumer talks to:\n *\n * - autocapture SDK plugin → instantiates one engine per init() call,\n * wires it into the click handler, and forwards remote-config updates\n * via `updateConfig`.\n * - app.amplitude.com tagging UI → fetches the customer's remote config\n * out-of-band and stands up a transient engine to compute selectors for\n * elements the user clicks in the iframe.\n * - Chrome extension visual tagger → reads the customer's already-live\n * engine off `window.amplitude.elementSelector` (when present) and\n * subscribes to `onConfigChange` so the extension's preview stays in\n * sync with the customer's runtime config.\n *\n * The factory deliberately takes a pre-resolved `ResolvedSelectorConfig` rather\n * than the raw remote payload — config resolution is a separate concern handled\n * by `resolveSelectorConfig`, and keeping it out of the engine constructor lets\n * dashboard / extension consumers stand up an engine from a static snapshot\n * without re-running the full resolver.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ResolvedSelectorConfig, SelectorEngine } from './types';\nimport { runOrchestrator, OrchestratorOptions } from './orchestrator';\nimport { fallbackCssPath } from './fallback-css-path';\n\nexport interface CreateSelectorEngineOptions {\n /** Optional document or shadow root for uniqueness checks. Defaults per-call to the target's owner document. */\n scope?: ParentNode;\n /** Optional override of the strategy chain. Primarily for testing / dashboard ad-hoc runs. */\n strategies?: OrchestratorOptions['strategies'];\n /**\n * Optional logger threaded through the orchestrator and the subscriber-fan-out\n * inside `updateConfig`. When provided, the engine surfaces malformed\n * selectors (`debug`) and listener exceptions (`warn`); when absent it stays\n * silent — preserving the legacy \"fire-and-forget\" semantics existing\n * consumers may rely on.\n */\n logger?: ElementSelectorLogger;\n}\n\n/**\n * Build a `SelectorEngine` bound to the supplied config.\n *\n * The returned engine is independent — calling the factory twice yields two\n * engines with separate config state and separate subscriber lists. This is the\n * shape the autocapture plugin wants (one engine per SDK instance) and the\n * shape the Chrome extension consumes off the page (the extension reads, never\n * writes).\n */\nexport function createSelectorEngine(\n initialConfig: ResolvedSelectorConfig,\n options: CreateSelectorEngineOptions = {},\n): SelectorEngine {\n let config: ResolvedSelectorConfig = initialConfig;\n const subscribers = new Set<(config: ResolvedSelectorConfig) => void>();\n const logger = options.logger;\n\n const orchestratorOptions: OrchestratorOptions = {\n strategies: options.strategies,\n scope: options.scope,\n logger,\n };\n\n return {\n generate(el: Element): string {\n // Try the strategy chain first.\n const composed = runOrchestrator(el, config, orchestratorOptions);\n if (composed !== null) {\n return composed;\n }\n // Strategy chain found nothing usable — fall back to the hardened\n // positional walker with the same scope used by the orchestrator.\n return fallbackCssPath(el, config, { scope: options.scope ?? el.ownerDocument ?? document });\n },\n\n getConfig(): Readonly<ResolvedSelectorConfig> {\n return config;\n },\n\n updateConfig(next: ResolvedSelectorConfig): void {\n config = next;\n // Notify subscribers in insertion order. Errors thrown by individual\n // subscribers are isolated so one bad listener can't break the others —\n // the extension's listener is the canonical consumer here and we don't\n // want SDK-side changes to cascade-fail because of an extension bug.\n for (const cb of subscribers) {\n try {\n cb(next);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: onConfigChange subscriber threw — ${message}`);\n }\n }\n },\n\n onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void {\n subscribers.add(cb);\n return () => {\n subscribers.delete(cb);\n };\n },\n };\n}\n"]} |
| /** | ||
| * Fallback CSS-path walker — the safety net for the v1 element-selector | ||
| * algorithm. | ||
| * | ||
| * When the orchestrator's strategy chain finds nothing usable across the full | ||
| * ancestor walk, the engine invokes this fallback. It produces a positional | ||
| * selector by walking up from the target to <html>, expressing each step as | ||
| * either an id anchor (when one survives the autogen filter) or a | ||
| * `tag:nth-of-type(n)` step. | ||
| * | ||
| * Key differences from the legacy autocapture `cssPath`: | ||
| * | ||
| * 1. Ids are filtered through `getStableId`, which honors the | ||
| * explicit-tracking-attribute suppression signal AND the autogenerated-id | ||
| * pattern pack. The legacy `cssPath` would happily anchor on `id=":r5:"` | ||
| * and produce selectors that never match across page loads. | ||
| * | ||
| * 2. Classes — when they're used at all for sibling disambiguation — are | ||
| * filtered through `filterClasses` against the unstable-class pattern | ||
| * pack. The current v1 implementation here doesn't emit classes (we lean | ||
| * entirely on `:nth-of-type` for sibling disambiguation), but the | ||
| * filtering helper is wired through so future iterations can opt back in | ||
| * surgically without re-deriving the policy. | ||
| * | ||
| * 3. A unique anchor id terminates the walk early — once we hit `body#main`, | ||
| * we don't need to keep walking up to `<html>`. | ||
| * | ||
| * 4. Ambiguous id anchors are skipped. Duplicate ids are invalid HTML but | ||
| * common enough in the wild that fallback must not emit selectors that | ||
| * resolve to the wrong target. | ||
| * | ||
| * Output format mirrors the orchestrator's output: `<anchor> > <descent>`. | ||
| * When no usable id is found anywhere on the walk, the fallback emits a | ||
| * pure-positional selector rooted at `html`. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { ResolvedSelectorConfig } from './types'; | ||
| export interface FallbackCssPathOptions { | ||
| /** Document or shadow root used for uniqueness checks. Defaults to the target's owner document. */ | ||
| scope?: ParentNode; | ||
| } | ||
| /** | ||
| * Build a positional CSS selector for `el`, using stable ids as anchors when | ||
| * available and falling back to `tag:nth-of-type(n)` for disambiguation. | ||
| * | ||
| * Honors `config.maxAncestorWalkDepth` defensively — once the depth limit is | ||
| * reached, the walker stops and returns whatever it has built so far rooted at | ||
| * the deepest reached ancestor. | ||
| */ | ||
| export declare function fallbackCssPath(el: Element, config: ResolvedSelectorConfig, options?: FallbackCssPathOptions): string; | ||
| //# sourceMappingURL=fallback-css-path.d.ts.map |
| {"version":3,"file":"fallback-css-path.d.ts","sourceRoot":"","sources":["../../src/fallback-css-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;AAEH,OAAO,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAIjD,MAAM,WAAW,sBAAsB;IACrC,mGAAmG;IACnG,KAAK,CAAC,EAAE,UAAU,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,sBAAsB,EAC9B,OAAO,GAAE,sBAA2B,GACnC,MAAM,CAgCR"} |
| /** | ||
| * Fallback CSS-path walker — the safety net for the v1 element-selector | ||
| * algorithm. | ||
| * | ||
| * When the orchestrator's strategy chain finds nothing usable across the full | ||
| * ancestor walk, the engine invokes this fallback. It produces a positional | ||
| * selector by walking up from the target to <html>, expressing each step as | ||
| * either an id anchor (when one survives the autogen filter) or a | ||
| * `tag:nth-of-type(n)` step. | ||
| * | ||
| * Key differences from the legacy autocapture `cssPath`: | ||
| * | ||
| * 1. Ids are filtered through `getStableId`, which honors the | ||
| * explicit-tracking-attribute suppression signal AND the autogenerated-id | ||
| * pattern pack. The legacy `cssPath` would happily anchor on `id=":r5:"` | ||
| * and produce selectors that never match across page loads. | ||
| * | ||
| * 2. Classes — when they're used at all for sibling disambiguation — are | ||
| * filtered through `filterClasses` against the unstable-class pattern | ||
| * pack. The current v1 implementation here doesn't emit classes (we lean | ||
| * entirely on `:nth-of-type` for sibling disambiguation), but the | ||
| * filtering helper is wired through so future iterations can opt back in | ||
| * surgically without re-deriving the policy. | ||
| * | ||
| * 3. A unique anchor id terminates the walk early — once we hit `body#main`, | ||
| * we don't need to keep walking up to `<html>`. | ||
| * | ||
| * 4. Ambiguous id anchors are skipped. Duplicate ids are invalid HTML but | ||
| * common enough in the wild that fallback must not emit selectors that | ||
| * resolve to the wrong target. | ||
| * | ||
| * Output format mirrors the orchestrator's output: `<anchor> > <descent>`. | ||
| * When no usable id is found anywhere on the walk, the fallback emits a | ||
| * pure-positional selector rooted at `html`. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { __read, __spreadArray } from "tslib"; | ||
| import { getStableId } from './helpers/get-stable-id'; | ||
| import { escapeIdForCss } from './helpers/escape-id'; | ||
| /** | ||
| * Build a positional CSS selector for `el`, using stable ids as anchors when | ||
| * available and falling back to `tag:nth-of-type(n)` for disambiguation. | ||
| * | ||
| * Honors `config.maxAncestorWalkDepth` defensively — once the depth limit is | ||
| * reached, the walker stops and returns whatever it has built so far rooted at | ||
| * the deepest reached ancestor. | ||
| */ | ||
| export function fallbackCssPath(el, config, options) { | ||
| var _a, _b; | ||
| if (options === void 0) { options = {}; } | ||
| var scope = (_b = (_a = options.scope) !== null && _a !== void 0 ? _a : el.ownerDocument) !== null && _b !== void 0 ? _b : document; | ||
| var segments = []; | ||
| var cursor = el; | ||
| var depth = 0; | ||
| while (cursor !== null) { | ||
| if (config.maxAncestorWalkDepth !== undefined && depth > config.maxAncestorWalkDepth) { | ||
| break; | ||
| } | ||
| var id = getStableId(cursor, config); | ||
| if (id !== null) { | ||
| // Anchor format mirrors the stableId strategy: `tag#id` with the id | ||
| // CSS-escaped. Only terminate if the composed selector uniquely resolves | ||
| // to the target; duplicate ids should fall through to positional steps. | ||
| var anchorSegment = "".concat(cursor.tagName.toLowerCase(), "#").concat(escapeIdForCss(id)); | ||
| var candidate = __spreadArray([anchorSegment], __read(segments), false).join(' > '); | ||
| if (isUniqueMatch(scope, candidate, el)) { | ||
| return candidate; | ||
| } | ||
| } | ||
| segments.unshift(stepFor(cursor)); | ||
| cursor = cursor.parentElement; | ||
| depth += 1; | ||
| } | ||
| // Walked all the way to the document root without finding an id. The first | ||
| // segment is whatever `<html>` produced (which is just `html` since it has | ||
| // no parent and no same-tag siblings). | ||
| return segments.join(' > '); | ||
| } | ||
| /** | ||
| * Build a single step. For an element with a parent: `tag:nth-of-type(n)`. | ||
| * For a root element (no parent — i.e. <html>): just `tag`. | ||
| * | ||
| * Class-based disambiguation is intentionally not emitted in v1 — see the | ||
| * design doc, "Why we don't use classes for sibling disambiguation". The | ||
| * `filterClasses` helper is imported in the package for future iterations. | ||
| */ | ||
| function stepFor(el) { | ||
| var tag = el.tagName.toLowerCase(); | ||
| var parent = el.parentElement; | ||
| if (parent === null) { | ||
| return tag; | ||
| } | ||
| var index = sameTypeIndex(el, parent); | ||
| return "".concat(tag, ":nth-of-type(").concat(index, ")"); | ||
| } | ||
| function sameTypeIndex(el, parent) { | ||
| var count = 0; | ||
| for (var i = 0; i < parent.children.length; i++) { | ||
| var sibling = parent.children[i]; | ||
| if (sibling.tagName === el.tagName) { | ||
| count += 1; | ||
| if (sibling === el) | ||
| return count; | ||
| } | ||
| } | ||
| return 1; | ||
| } | ||
| function isUniqueMatch(scope, selector, el) { | ||
| var matches; | ||
| try { | ||
| matches = scope.querySelectorAll(selector); | ||
| } | ||
| catch (_e) { | ||
| return false; | ||
| } | ||
| return matches.length === 1 && matches[0] === el; | ||
| } | ||
| //# sourceMappingURL=fallback-css-path.js.map |
| {"version":3,"file":"fallback-css-path.js","sourceRoot":"","sources":["../../src/fallback-css-path.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;;AAGH,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAOrD;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAC7B,EAAW,EACX,MAA8B,EAC9B,OAAoC;;IAApC,wBAAA,EAAA,YAAoC;IAEpC,IAAM,KAAK,GAAe,MAAA,MAAA,OAAO,CAAC,KAAK,mCAAI,EAAE,CAAC,aAAa,mCAAI,QAAQ,CAAC;IACxE,IAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,IAAI,MAAM,GAAmB,EAAE,CAAC;IAChC,IAAI,KAAK,GAAG,CAAC,CAAC;IAEd,OAAO,MAAM,KAAK,IAAI,EAAE;QACtB,IAAI,MAAM,CAAC,oBAAoB,KAAK,SAAS,IAAI,KAAK,GAAG,MAAM,CAAC,oBAAoB,EAAE;YACpF,MAAM;SACP;QAED,IAAM,EAAE,GAAG,WAAW,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;QACvC,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,oEAAoE;YACpE,yEAAyE;YACzE,wEAAwE;YACxE,IAAM,aAAa,GAAG,UAAG,MAAM,CAAC,OAAO,CAAC,WAAW,EAAE,cAAI,cAAc,CAAC,EAAE,CAAC,CAAE,CAAC;YAC9E,IAAM,SAAS,GAAG,eAAC,aAAa,UAAK,QAAQ,UAAE,IAAI,CAAC,KAAK,CAAC,CAAC;YAC3D,IAAI,aAAa,CAAC,KAAK,EAAE,SAAS,EAAE,EAAE,CAAC,EAAE;gBACvC,OAAO,SAAS,CAAC;aAClB;SACF;QAED,QAAQ,CAAC,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAClC,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC;QAC9B,KAAK,IAAI,CAAC,CAAC;KACZ;IAED,2EAA2E;IAC3E,2EAA2E;IAC3E,uCAAuC;IACvC,OAAO,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AAC9B,CAAC;AAED;;;;;;;GAOG;AACH,SAAS,OAAO,CAAC,EAAW;IAC1B,IAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IACrC,IAAM,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC;IAChC,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,OAAO,GAAG,CAAC;KACZ;IACD,IAAM,KAAK,GAAG,aAAa,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,UAAG,GAAG,0BAAgB,KAAK,MAAG,CAAC;AACxC,CAAC;AAED,SAAS,aAAa,CAAC,EAAW,EAAE,MAAe;IACjD,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QAC/C,IAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,OAAO,CAAC,OAAO,KAAK,EAAE,CAAC,OAAO,EAAE;YAClC,KAAK,IAAI,CAAC,CAAC;YACX,IAAI,OAAO,KAAK,EAAE;gBAAE,OAAO,KAAK,CAAC;SAClC;KACF;IACD,OAAO,CAAC,CAAC;AACX,CAAC;AAED,SAAS,aAAa,CAAC,KAAiB,EAAE,QAAgB,EAAE,EAAW;IACrE,IAAI,OAA4B,CAAC;IACjC,IAAI;QACF,OAAO,GAAG,KAAK,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;KAC5C;IAAC,OAAO,EAAE,EAAE;QACX,OAAO,KAAK,CAAC;KACd;IACD,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;AACnD,CAAC","sourcesContent":["/**\n * Fallback CSS-path walker — the safety net for the v1 element-selector\n * algorithm.\n *\n * When the orchestrator's strategy chain finds nothing usable across the full\n * ancestor walk, the engine invokes this fallback. It produces a positional\n * selector by walking up from the target to <html>, expressing each step as\n * either an id anchor (when one survives the autogen filter) or a\n * `tag:nth-of-type(n)` step.\n *\n * Key differences from the legacy autocapture `cssPath`:\n *\n * 1. Ids are filtered through `getStableId`, which honors the\n * explicit-tracking-attribute suppression signal AND the autogenerated-id\n * pattern pack. The legacy `cssPath` would happily anchor on `id=\":r5:\"`\n * and produce selectors that never match across page loads.\n *\n * 2. Classes — when they're used at all for sibling disambiguation — are\n * filtered through `filterClasses` against the unstable-class pattern\n * pack. The current v1 implementation here doesn't emit classes (we lean\n * entirely on `:nth-of-type` for sibling disambiguation), but the\n * filtering helper is wired through so future iterations can opt back in\n * surgically without re-deriving the policy.\n *\n * 3. A unique anchor id terminates the walk early — once we hit `body#main`,\n * we don't need to keep walking up to `<html>`.\n *\n * 4. Ambiguous id anchors are skipped. Duplicate ids are invalid HTML but\n * common enough in the wild that fallback must not emit selectors that\n * resolve to the wrong target.\n *\n * Output format mirrors the orchestrator's output: `<anchor> > <descent>`.\n * When no usable id is found anywhere on the walk, the fallback emits a\n * pure-positional selector rooted at `html`.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ResolvedSelectorConfig } from './types';\nimport { getStableId } from './helpers/get-stable-id';\nimport { escapeIdForCss } from './helpers/escape-id';\n\nexport interface FallbackCssPathOptions {\n /** Document or shadow root used for uniqueness checks. Defaults to the target's owner document. */\n scope?: ParentNode;\n}\n\n/**\n * Build a positional CSS selector for `el`, using stable ids as anchors when\n * available and falling back to `tag:nth-of-type(n)` for disambiguation.\n *\n * Honors `config.maxAncestorWalkDepth` defensively — once the depth limit is\n * reached, the walker stops and returns whatever it has built so far rooted at\n * the deepest reached ancestor.\n */\nexport function fallbackCssPath(\n el: Element,\n config: ResolvedSelectorConfig,\n options: FallbackCssPathOptions = {},\n): string {\n const scope: ParentNode = options.scope ?? el.ownerDocument ?? document;\n const segments: string[] = [];\n let cursor: Element | null = el;\n let depth = 0;\n\n while (cursor !== null) {\n if (config.maxAncestorWalkDepth !== undefined && depth > config.maxAncestorWalkDepth) {\n break;\n }\n\n const id = getStableId(cursor, config);\n if (id !== null) {\n // Anchor format mirrors the stableId strategy: `tag#id` with the id\n // CSS-escaped. Only terminate if the composed selector uniquely resolves\n // to the target; duplicate ids should fall through to positional steps.\n const anchorSegment = `${cursor.tagName.toLowerCase()}#${escapeIdForCss(id)}`;\n const candidate = [anchorSegment, ...segments].join(' > ');\n if (isUniqueMatch(scope, candidate, el)) {\n return candidate;\n }\n }\n\n segments.unshift(stepFor(cursor));\n cursor = cursor.parentElement;\n depth += 1;\n }\n\n // Walked all the way to the document root without finding an id. The first\n // segment is whatever `<html>` produced (which is just `html` since it has\n // no parent and no same-tag siblings).\n return segments.join(' > ');\n}\n\n/**\n * Build a single step. For an element with a parent: `tag:nth-of-type(n)`.\n * For a root element (no parent — i.e. <html>): just `tag`.\n *\n * Class-based disambiguation is intentionally not emitted in v1 — see the\n * design doc, \"Why we don't use classes for sibling disambiguation\". The\n * `filterClasses` helper is imported in the package for future iterations.\n */\nfunction stepFor(el: Element): string {\n const tag = el.tagName.toLowerCase();\n const parent = el.parentElement;\n if (parent === null) {\n return tag;\n }\n const index = sameTypeIndex(el, parent);\n return `${tag}:nth-of-type(${index})`;\n}\n\nfunction sameTypeIndex(el: Element, parent: Element): number {\n let count = 0;\n for (let i = 0; i < parent.children.length; i++) {\n const sibling = parent.children[i];\n if (sibling.tagName === el.tagName) {\n count += 1;\n if (sibling === el) return count;\n }\n }\n return 1;\n}\n\nfunction isUniqueMatch(scope: ParentNode, selector: string, el: Element): boolean {\n let matches: NodeListOf<Element>;\n try {\n matches = scope.querySelectorAll(selector);\n } catch (_e) {\n return false;\n }\n return matches.length === 1 && matches[0] === el;\n}\n"]} |
| /** | ||
| * Positional descent builder. | ||
| * | ||
| * Given an anchor element (the result of pass 2's walk in the orchestration PR) | ||
| * and the trail of intermediate elements between the anchor and the original | ||
| * click target, produce the CSS-selector descent string. Each step in the | ||
| * descent uses `tag:nth-of-type(n)` so the resulting selector resolves | ||
| * unambiguously to the original target regardless of class-state churn. | ||
| * | ||
| * Matches ContentSquare's "position within identical markers" convention. | ||
| * | ||
| * Example: | ||
| * | ||
| * describeRelative(anchor, [<section>, <ul>, <li>]) | ||
| * → "section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)" | ||
| * | ||
| * Combined with the anchor selector (`anchor#some-id`) by the orchestrator: | ||
| * | ||
| * "anchor#some-id > section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)" | ||
| * | ||
| * The `anchor` parameter is accepted for symmetry with the orchestrator's call | ||
| * site but isn't currently used — the function only needs each trail element's | ||
| * own parent context. We keep the signature so subsequent work can reference | ||
| * the anchor when extending the descent format (e.g., to optimize bare | ||
| * `:nth-of-type(1)` away when there's only one of that type). | ||
| */ | ||
| export declare function describeRelative(_anchor: Element, trail: Element[]): string; | ||
| //# sourceMappingURL=describe-relative.d.ts.map |
| {"version":3,"file":"describe-relative.d.ts","sourceRoot":"","sources":["../../../src/helpers/describe-relative.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,GAAG,MAAM,CAE3E"} |
| /** | ||
| * Positional descent builder. | ||
| * | ||
| * Given an anchor element (the result of pass 2's walk in the orchestration PR) | ||
| * and the trail of intermediate elements between the anchor and the original | ||
| * click target, produce the CSS-selector descent string. Each step in the | ||
| * descent uses `tag:nth-of-type(n)` so the resulting selector resolves | ||
| * unambiguously to the original target regardless of class-state churn. | ||
| * | ||
| * Matches ContentSquare's "position within identical markers" convention. | ||
| * | ||
| * Example: | ||
| * | ||
| * describeRelative(anchor, [<section>, <ul>, <li>]) | ||
| * → "section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)" | ||
| * | ||
| * Combined with the anchor selector (`anchor#some-id`) by the orchestrator: | ||
| * | ||
| * "anchor#some-id > section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)" | ||
| * | ||
| * The `anchor` parameter is accepted for symmetry with the orchestrator's call | ||
| * site but isn't currently used — the function only needs each trail element's | ||
| * own parent context. We keep the signature so subsequent work can reference | ||
| * the anchor when extending the descent format (e.g., to optimize bare | ||
| * `:nth-of-type(1)` away when there's only one of that type). | ||
| */ | ||
| export function describeRelative(_anchor, trail) { | ||
| return trail.map(stepFor).join(' > '); | ||
| } | ||
| function stepFor(el) { | ||
| var tag = el.tagName.toLowerCase(); | ||
| var parent = el.parentElement; | ||
| if (parent === null) { | ||
| // Detached element or root — emit just the tag. The orchestrator shouldn't | ||
| // call us with a detached trail, but we don't want to crash if it does. | ||
| return tag; | ||
| } | ||
| var index = sameTypeIndex(el, parent); | ||
| return "".concat(tag, ":nth-of-type(").concat(index, ")"); | ||
| } | ||
| function sameTypeIndex(el, parent) { | ||
| // 1-based index among same-tag element siblings, mirroring :nth-of-type semantics. | ||
| var count = 0; | ||
| for (var i = 0; i < parent.children.length; i++) { | ||
| var sibling = parent.children[i]; | ||
| if (sibling.tagName === el.tagName) { | ||
| count += 1; | ||
| if (sibling === el) | ||
| return count; | ||
| } | ||
| } | ||
| // Element wasn't found among its parent's children — shouldn't happen for a | ||
| // live element. Return 1 as a defensive fallback rather than throwing. | ||
| return 1; | ||
| } | ||
| //# sourceMappingURL=describe-relative.js.map |
| {"version":3,"file":"describe-relative.js","sourceRoot":"","sources":["../../../src/helpers/describe-relative.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,gBAAgB,CAAC,OAAgB,EAAE,KAAgB;IACjE,OAAO,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;AACxC,CAAC;AAED,SAAS,OAAO,CAAC,EAAW;IAC1B,IAAM,GAAG,GAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC;IACrC,IAAM,MAAM,GAAG,EAAE,CAAC,aAAa,CAAC;IAChC,IAAI,MAAM,KAAK,IAAI,EAAE;QACnB,2EAA2E;QAC3E,wEAAwE;QACxE,OAAO,GAAG,CAAC;KACZ;IACD,IAAM,KAAK,GAAG,aAAa,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;IACxC,OAAO,UAAG,GAAG,0BAAgB,KAAK,MAAG,CAAC;AACxC,CAAC;AAED,SAAS,aAAa,CAAC,EAAW,EAAE,MAAe;IACjD,mFAAmF;IACnF,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;QAC/C,IAAM,OAAO,GAAG,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC;QACnC,IAAI,OAAO,CAAC,OAAO,KAAK,EAAE,CAAC,OAAO,EAAE;YAClC,KAAK,IAAI,CAAC,CAAC;YACX,IAAI,OAAO,KAAK,EAAE;gBAAE,OAAO,KAAK,CAAC;SAClC;KACF;IACD,4EAA4E;IAC5E,uEAAuE;IACvE,OAAO,CAAC,CAAC;AACX,CAAC","sourcesContent":["/**\n * Positional descent builder.\n *\n * Given an anchor element (the result of pass 2's walk in the orchestration PR)\n * and the trail of intermediate elements between the anchor and the original\n * click target, produce the CSS-selector descent string. Each step in the\n * descent uses `tag:nth-of-type(n)` so the resulting selector resolves\n * unambiguously to the original target regardless of class-state churn.\n *\n * Matches ContentSquare's \"position within identical markers\" convention.\n *\n * Example:\n *\n * describeRelative(anchor, [<section>, <ul>, <li>])\n * → \"section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)\"\n *\n * Combined with the anchor selector (`anchor#some-id`) by the orchestrator:\n *\n * \"anchor#some-id > section:nth-of-type(1) > ul:nth-of-type(1) > li:nth-of-type(3)\"\n *\n * The `anchor` parameter is accepted for symmetry with the orchestrator's call\n * site but isn't currently used — the function only needs each trail element's\n * own parent context. We keep the signature so subsequent work can reference\n * the anchor when extending the descent format (e.g., to optimize bare\n * `:nth-of-type(1)` away when there's only one of that type).\n */\nexport function describeRelative(_anchor: Element, trail: Element[]): string {\n return trail.map(stepFor).join(' > ');\n}\n\nfunction stepFor(el: Element): string {\n const tag = el.tagName.toLowerCase();\n const parent = el.parentElement;\n if (parent === null) {\n // Detached element or root — emit just the tag. The orchestrator shouldn't\n // call us with a detached trail, but we don't want to crash if it does.\n return tag;\n }\n const index = sameTypeIndex(el, parent);\n return `${tag}:nth-of-type(${index})`;\n}\n\nfunction sameTypeIndex(el: Element, parent: Element): number {\n // 1-based index among same-tag element siblings, mirroring :nth-of-type semantics.\n let count = 0;\n for (let i = 0; i < parent.children.length; i++) {\n const sibling = parent.children[i];\n if (sibling.tagName === el.tagName) {\n count += 1;\n if (sibling === el) return count;\n }\n }\n // Element wasn't found among its parent's children — shouldn't happen for a\n // live element. Return 1 as a defensive fallback rather than throwing.\n return 1;\n}\n"]} |
| /** | ||
| * Escape a string for use as a CSS identifier. | ||
| * | ||
| * Uses the native CSS.escape implementation when available, with the CSSOM | ||
| * algorithm inlined for runtimes that do not expose it (notably jsdom). | ||
| */ | ||
| export declare function escapeCssIdentifier(value: string): string; | ||
| //# sourceMappingURL=escape-css-identifier.d.ts.map |
| {"version":3,"file":"escape-css-identifier.d.ts","sourceRoot":"","sources":["../../../src/helpers/escape-css-identifier.ts"],"names":[],"mappings":"AAQA;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAiDzD"} |
| /* eslint-disable no-restricted-globals */ | ||
| /** | ||
| * Escape a string for use as a CSS identifier. | ||
| * | ||
| * Uses the native CSS.escape implementation when available, with the CSSOM | ||
| * algorithm inlined for runtimes that do not expose it (notably jsdom). | ||
| */ | ||
| export function escapeCssIdentifier(value) { | ||
| var css = globalThis.CSS; | ||
| if (css && typeof css.escape === 'function') { | ||
| return css.escape(value); | ||
| } | ||
| var string = String(value); | ||
| var length = string.length; | ||
| var result = ''; | ||
| for (var index = 0; index < length; index++) { | ||
| var codeUnit = string.charCodeAt(index); | ||
| if (codeUnit === 0x0000) { | ||
| result += '\uFFFD'; | ||
| continue; | ||
| } | ||
| if ((codeUnit >= 0x0001 && codeUnit <= 0x001f) || | ||
| codeUnit === 0x007f || | ||
| (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) || | ||
| (index === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && string.charCodeAt(0) === 0x002d)) { | ||
| result += "\\".concat(codeUnit.toString(16), " "); | ||
| continue; | ||
| } | ||
| if (index === 0 && length === 1 && codeUnit === 0x002d) { | ||
| result += '\\-'; | ||
| continue; | ||
| } | ||
| if (codeUnit >= 0x0080 || | ||
| codeUnit === 0x002d || | ||
| codeUnit === 0x005f || | ||
| (codeUnit >= 0x0030 && codeUnit <= 0x0039) || | ||
| (codeUnit >= 0x0041 && codeUnit <= 0x005a) || | ||
| (codeUnit >= 0x0061 && codeUnit <= 0x007a)) { | ||
| result += string.charAt(index); | ||
| continue; | ||
| } | ||
| result += "\\".concat(string.charAt(index)); | ||
| } | ||
| return result; | ||
| } | ||
| //# sourceMappingURL=escape-css-identifier.js.map |
| {"version":3,"file":"escape-css-identifier.js","sourceRoot":"","sources":["../../../src/helpers/escape-css-identifier.ts"],"names":[],"mappings":"AAAA,0CAA0C;AAQ1C;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,KAAa;IAC/C,IAAM,GAAG,GAAI,UAA8B,CAAC,GAAG,CAAC;IAChD,IAAI,GAAG,IAAI,OAAO,GAAG,CAAC,MAAM,KAAK,UAAU,EAAE;QAC3C,OAAO,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;KAC1B;IAED,IAAM,MAAM,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAC7B,IAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC;IAC7B,IAAI,MAAM,GAAG,EAAE,CAAC;IAEhB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,EAAE,EAAE;QAC3C,IAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC;QAE1C,IAAI,QAAQ,KAAK,MAAM,EAAE;YACvB,MAAM,IAAI,QAAQ,CAAC;YACnB,SAAS;SACV;QAED,IACE,CAAC,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC;YAC1C,QAAQ,KAAK,MAAM;YACnB,CAAC,KAAK,KAAK,CAAC,IAAI,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC;YACzD,CAAC,KAAK,KAAK,CAAC,IAAI,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,IAAI,MAAM,CAAC,UAAU,CAAC,CAAC,CAAC,KAAK,MAAM,CAAC,EAC5F;YACA,MAAM,IAAI,YAAK,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAG,CAAC;YACxC,SAAS;SACV;QAED,IAAI,KAAK,KAAK,CAAC,IAAI,MAAM,KAAK,CAAC,IAAI,QAAQ,KAAK,MAAM,EAAE;YACtD,MAAM,IAAI,KAAK,CAAC;YAChB,SAAS;SACV;QAED,IACE,QAAQ,IAAI,MAAM;YAClB,QAAQ,KAAK,MAAM;YACnB,QAAQ,KAAK,MAAM;YACnB,CAAC,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC;YAC1C,CAAC,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC;YAC1C,CAAC,QAAQ,IAAI,MAAM,IAAI,QAAQ,IAAI,MAAM,CAAC,EAC1C;YACA,MAAM,IAAI,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC/B,SAAS;SACV;QAED,MAAM,IAAI,YAAK,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAE,CAAC;KACvC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["/* eslint-disable no-restricted-globals */\n\ntype CssEscapeGlobal = {\n CSS?: {\n escape?: (value: string) => string;\n };\n};\n\n/**\n * Escape a string for use as a CSS identifier.\n *\n * Uses the native CSS.escape implementation when available, with the CSSOM\n * algorithm inlined for runtimes that do not expose it (notably jsdom).\n */\nexport function escapeCssIdentifier(value: string): string {\n const css = (globalThis as CssEscapeGlobal).CSS;\n if (css && typeof css.escape === 'function') {\n return css.escape(value);\n }\n\n const string = String(value);\n const length = string.length;\n let result = '';\n\n for (let index = 0; index < length; index++) {\n const codeUnit = string.charCodeAt(index);\n\n if (codeUnit === 0x0000) {\n result += '\\uFFFD';\n continue;\n }\n\n if (\n (codeUnit >= 0x0001 && codeUnit <= 0x001f) ||\n codeUnit === 0x007f ||\n (index === 0 && codeUnit >= 0x0030 && codeUnit <= 0x0039) ||\n (index === 1 && codeUnit >= 0x0030 && codeUnit <= 0x0039 && string.charCodeAt(0) === 0x002d)\n ) {\n result += `\\\\${codeUnit.toString(16)} `;\n continue;\n }\n\n if (index === 0 && length === 1 && codeUnit === 0x002d) {\n result += '\\\\-';\n continue;\n }\n\n if (\n codeUnit >= 0x0080 ||\n codeUnit === 0x002d ||\n codeUnit === 0x005f ||\n (codeUnit >= 0x0030 && codeUnit <= 0x0039) ||\n (codeUnit >= 0x0041 && codeUnit <= 0x005a) ||\n (codeUnit >= 0x0061 && codeUnit <= 0x007a)\n ) {\n result += string.charAt(index);\n continue;\n }\n\n result += `\\\\${string.charAt(index)}`;\n }\n\n return result;\n}\n"]} |
| /** Escape an element id for the `tag#<id>` selector form shared by strategies and fallback. */ | ||
| export declare function escapeIdForCss(id: string): string; | ||
| //# sourceMappingURL=escape-id.d.ts.map |
| {"version":3,"file":"escape-id.d.ts","sourceRoot":"","sources":["../../../src/helpers/escape-id.ts"],"names":[],"mappings":"AAEA,+FAA+F;AAC/F,wBAAgB,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CAEjD"} |
| import { escapeCssIdentifier } from './escape-css-identifier'; | ||
| /** Escape an element id for the `tag#<id>` selector form shared by strategies and fallback. */ | ||
| export function escapeIdForCss(id) { | ||
| return escapeCssIdentifier(id); | ||
| } | ||
| //# sourceMappingURL=escape-id.js.map |
| {"version":3,"file":"escape-id.js","sourceRoot":"","sources":["../../../src/helpers/escape-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,yBAAyB,CAAC;AAE9D,+FAA+F;AAC/F,MAAM,UAAU,cAAc,CAAC,EAAU;IACvC,OAAO,mBAAmB,CAAC,EAAE,CAAC,CAAC;AACjC,CAAC","sourcesContent":["import { escapeCssIdentifier } from './escape-css-identifier';\n\n/** Escape an element id for the `tag#<id>` selector form shared by strategies and fallback. */\nexport function escapeIdForCss(id: string): string {\n return escapeCssIdentifier(id);\n}\n"]} |
| import { ResolvedSelectorConfig } from '../types'; | ||
| /** | ||
| * Shared helper that the `stableId` strategy (in this PR) and the | ||
| * `fallback-css-path` walker (in the orchestration PR) both consult to decide | ||
| * whether an element's id is usable as a selector anchor. | ||
| * | ||
| * Resolution order: | ||
| * | ||
| * 1. Check the customer's explicit-tracking attribute. If set with an empty | ||
| * value (e.g. `data-amp-track-id=""`), that's an explicit suppression | ||
| * signal — the customer is telling us to ignore this element's id even | ||
| * if it would otherwise look stable. Return null. | ||
| * 2. Read the element's id. If absent or empty, return null. | ||
| * 3. Check the id against the autogenerated-id pattern pack. If matched, | ||
| * return null (the algorithm should walk past this element). | ||
| * 4. Otherwise return the id. | ||
| * | ||
| * The single point of consultation means the strategy and the fallback always | ||
| * agree on what's a usable id — no chance of one component filtering an id | ||
| * while another uses it anyway. | ||
| */ | ||
| export declare function getStableId(el: Element, cfg: ResolvedSelectorConfig): string | null; | ||
| //# sourceMappingURL=get-stable-id.d.ts.map |
| {"version":3,"file":"get-stable-id.d.ts","sourceRoot":"","sources":["../../../src/helpers/get-stable-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,UAAU,CAAC;AAGlD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,WAAW,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,sBAAsB,GAAG,MAAM,GAAG,IAAI,CAoBnF"} |
| import { isStableId } from '../patterns/autogenerated-ids'; | ||
| /** | ||
| * Shared helper that the `stableId` strategy (in this PR) and the | ||
| * `fallback-css-path` walker (in the orchestration PR) both consult to decide | ||
| * whether an element's id is usable as a selector anchor. | ||
| * | ||
| * Resolution order: | ||
| * | ||
| * 1. Check the customer's explicit-tracking attribute. If set with an empty | ||
| * value (e.g. `data-amp-track-id=""`), that's an explicit suppression | ||
| * signal — the customer is telling us to ignore this element's id even | ||
| * if it would otherwise look stable. Return null. | ||
| * 2. Read the element's id. If absent or empty, return null. | ||
| * 3. Check the id against the autogenerated-id pattern pack. If matched, | ||
| * return null (the algorithm should walk past this element). | ||
| * 4. Otherwise return the id. | ||
| * | ||
| * The single point of consultation means the strategy and the fallback always | ||
| * agree on what's a usable id — no chance of one component filtering an id | ||
| * while another uses it anyway. | ||
| */ | ||
| export function getStableId(el, cfg) { | ||
| // 1. Suppression signal: empty explicit-tracking attribute on this element. | ||
| var trackAttr = el.getAttribute(cfg.explicitTrackingAttribute); | ||
| if (trackAttr !== null && trackAttr === '') { | ||
| return null; | ||
| } | ||
| // 2. Id presence. | ||
| var id = el.getAttribute('id'); | ||
| if (id === null || id === '') { | ||
| return null; | ||
| } | ||
| // 3. Autogenerated-pattern filter. | ||
| if (!isStableId(id, cfg.autogeneratedIdPatterns)) { | ||
| return null; | ||
| } | ||
| // 4. Stable. | ||
| return id; | ||
| } | ||
| //# sourceMappingURL=get-stable-id.js.map |
| {"version":3,"file":"get-stable-id.js","sourceRoot":"","sources":["../../../src/helpers/get-stable-id.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,+BAA+B,CAAC;AAE3D;;;;;;;;;;;;;;;;;;;GAmBG;AACH,MAAM,UAAU,WAAW,CAAC,EAAW,EAAE,GAA2B;IAClE,4EAA4E;IAC5E,IAAM,SAAS,GAAG,EAAE,CAAC,YAAY,CAAC,GAAG,CAAC,yBAAyB,CAAC,CAAC;IACjE,IAAI,SAAS,KAAK,IAAI,IAAI,SAAS,KAAK,EAAE,EAAE;QAC1C,OAAO,IAAI,CAAC;KACb;IAED,kBAAkB;IAClB,IAAM,EAAE,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACjC,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,EAAE,EAAE;QAC5B,OAAO,IAAI,CAAC;KACb;IAED,mCAAmC;IACnC,IAAI,CAAC,UAAU,CAAC,EAAE,EAAE,GAAG,CAAC,uBAAuB,CAAC,EAAE;QAChD,OAAO,IAAI,CAAC;KACb;IAED,aAAa;IACb,OAAO,EAAE,CAAC;AACZ,CAAC","sourcesContent":["import { ResolvedSelectorConfig } from '../types';\nimport { isStableId } from '../patterns/autogenerated-ids';\n\n/**\n * Shared helper that the `stableId` strategy (in this PR) and the\n * `fallback-css-path` walker (in the orchestration PR) both consult to decide\n * whether an element's id is usable as a selector anchor.\n *\n * Resolution order:\n *\n * 1. Check the customer's explicit-tracking attribute. If set with an empty\n * value (e.g. `data-amp-track-id=\"\"`), that's an explicit suppression\n * signal — the customer is telling us to ignore this element's id even\n * if it would otherwise look stable. Return null.\n * 2. Read the element's id. If absent or empty, return null.\n * 3. Check the id against the autogenerated-id pattern pack. If matched,\n * return null (the algorithm should walk past this element).\n * 4. Otherwise return the id.\n *\n * The single point of consultation means the strategy and the fallback always\n * agree on what's a usable id — no chance of one component filtering an id\n * while another uses it anyway.\n */\nexport function getStableId(el: Element, cfg: ResolvedSelectorConfig): string | null {\n // 1. Suppression signal: empty explicit-tracking attribute on this element.\n const trackAttr = el.getAttribute(cfg.explicitTrackingAttribute);\n if (trackAttr !== null && trackAttr === '') {\n return null;\n }\n\n // 2. Id presence.\n const id = el.getAttribute('id');\n if (id === null || id === '') {\n return null;\n }\n\n // 3. Autogenerated-pattern filter.\n if (!isStableId(id, cfg.autogeneratedIdPatterns)) {\n return null;\n }\n\n // 4. Stable.\n return id;\n}\n"]} |
| /** | ||
| * @amplitude/element-selector | ||
| * | ||
| * Shared element-selector algorithm consumed by autocapture, the | ||
| * app.amplitude.com tagging UI, and the Chrome extension visual tagger. | ||
| * | ||
| * This file is the package's public API surface. The orchestrator, fallback, | ||
| * config resolver, and engine factory compose on top of the primitives | ||
| * (types, pattern packs, helpers, strategies) to produce the runtime engine | ||
| * consumers actually call. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| export declare const PACKAGE_NAME = "@amplitude/element-selector"; | ||
| export type { Strategy, StrategyContext, ResolvedSelectorConfig, ElementSelectorRemoteConfig, ElementSelectorLogger, SelectorEngine, } from './types'; | ||
| export { DEFAULT_AUTOGENERATED_ID_PATTERNS, compile as compileAutogeneratedIdPatterns, isStableId, } from './patterns/autogenerated-ids'; | ||
| export { DEFAULT_UNSTABLE_CLASS_PATTERNS, compile as compileUnstableClassPatterns, filterClasses, } from './patterns/unstable-classes'; | ||
| export { getStableId } from './helpers/get-stable-id'; | ||
| export { describeRelative } from './helpers/describe-relative'; | ||
| export { explicitTrackingAttribute } from './strategies/explicit-tracking-attribute'; | ||
| export { stableId as stableIdStrategy } from './strategies/stable-id'; | ||
| export { runOrchestrator, DEFAULT_STRATEGIES } from './orchestrator'; | ||
| export type { OrchestratorOptions } from './orchestrator'; | ||
| export { fallbackCssPath } from './fallback-css-path'; | ||
| export { resolveSelectorConfig, DEFAULT_RESOLVED_CONFIG } from './config/resolve-config'; | ||
| export { createSelectorEngine } from './engine'; | ||
| export type { CreateSelectorEngineOptions } from './engine'; | ||
| //# sourceMappingURL=index.d.ts.map |
| {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,eAAO,MAAM,YAAY,gCAAgC,CAAC;AAG1D,YAAY,EACV,QAAQ,EACR,eAAe,EACf,sBAAsB,EACtB,2BAA2B,EAC3B,qBAAqB,EACrB,cAAc,GACf,MAAM,SAAS,CAAC;AAGjB,OAAO,EACL,iCAAiC,EACjC,OAAO,IAAI,8BAA8B,EACzC,UAAU,GACX,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,+BAA+B,EAC/B,OAAO,IAAI,4BAA4B,EACvC,aAAa,GACd,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAG/D,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,OAAO,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAKtE,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AACrE,YAAY,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAGtD,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAGzF,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC;AAChD,YAAY,EAAE,2BAA2B,EAAE,MAAM,UAAU,CAAC"} |
| /** | ||
| * @amplitude/element-selector | ||
| * | ||
| * Shared element-selector algorithm consumed by autocapture, the | ||
| * app.amplitude.com tagging UI, and the Chrome extension visual tagger. | ||
| * | ||
| * This file is the package's public API surface. The orchestrator, fallback, | ||
| * config resolver, and engine factory compose on top of the primitives | ||
| * (types, pattern packs, helpers, strategies) to produce the runtime engine | ||
| * consumers actually call. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| export var PACKAGE_NAME = '@amplitude/element-selector'; | ||
| // ===== Pattern packs ===== | ||
| export { DEFAULT_AUTOGENERATED_ID_PATTERNS, compile as compileAutogeneratedIdPatterns, isStableId, } from './patterns/autogenerated-ids'; | ||
| export { DEFAULT_UNSTABLE_CLASS_PATTERNS, compile as compileUnstableClassPatterns, filterClasses, } from './patterns/unstable-classes'; | ||
| // ===== Helpers ===== | ||
| export { getStableId } from './helpers/get-stable-id'; | ||
| export { describeRelative } from './helpers/describe-relative'; | ||
| // ===== Strategies ===== | ||
| export { explicitTrackingAttribute } from './strategies/explicit-tracking-attribute'; | ||
| export { stableId as stableIdStrategy } from './strategies/stable-id'; | ||
| // ===== Orchestrator + fallback ===== | ||
| // Exposed publicly so the dashboard / Chrome extension can call them ad-hoc | ||
| // without instantiating a full engine — useful for preview / debugging flows. | ||
| export { runOrchestrator, DEFAULT_STRATEGIES } from './orchestrator'; | ||
| export { fallbackCssPath } from './fallback-css-path'; | ||
| // ===== Config resolver ===== | ||
| export { resolveSelectorConfig, DEFAULT_RESOLVED_CONFIG } from './config/resolve-config'; | ||
| // ===== Engine factory ===== | ||
| export { createSelectorEngine } from './engine'; | ||
| //# sourceMappingURL=index.js.map |
| {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAEH,MAAM,CAAC,IAAM,YAAY,GAAG,6BAA6B,CAAC;AAY1D,4BAA4B;AAC5B,OAAO,EACL,iCAAiC,EACjC,OAAO,IAAI,8BAA8B,EACzC,UAAU,GACX,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EACL,+BAA+B,EAC/B,OAAO,IAAI,4BAA4B,EACvC,aAAa,GACd,MAAM,6BAA6B,CAAC;AAErC,sBAAsB;AACtB,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAE/D,yBAAyB;AACzB,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,OAAO,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AAEtE,sCAAsC;AACtC,4EAA4E;AAC5E,8EAA8E;AAC9E,OAAO,EAAE,eAAe,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC;AAErE,OAAO,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAEtD,8BAA8B;AAC9B,OAAO,EAAE,qBAAqB,EAAE,uBAAuB,EAAE,MAAM,yBAAyB,CAAC;AAEzF,6BAA6B;AAC7B,OAAO,EAAE,oBAAoB,EAAE,MAAM,UAAU,CAAC","sourcesContent":["/**\n * @amplitude/element-selector\n *\n * Shared element-selector algorithm consumed by autocapture, the\n * app.amplitude.com tagging UI, and the Chrome extension visual tagger.\n *\n * This file is the package's public API surface. The orchestrator, fallback,\n * config resolver, and engine factory compose on top of the primitives\n * (types, pattern packs, helpers, strategies) to produce the runtime engine\n * consumers actually call.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nexport const PACKAGE_NAME = '@amplitude/element-selector';\n\n// ===== Types =====\nexport type {\n Strategy,\n StrategyContext,\n ResolvedSelectorConfig,\n ElementSelectorRemoteConfig,\n ElementSelectorLogger,\n SelectorEngine,\n} from './types';\n\n// ===== Pattern packs =====\nexport {\n DEFAULT_AUTOGENERATED_ID_PATTERNS,\n compile as compileAutogeneratedIdPatterns,\n isStableId,\n} from './patterns/autogenerated-ids';\n\nexport {\n DEFAULT_UNSTABLE_CLASS_PATTERNS,\n compile as compileUnstableClassPatterns,\n filterClasses,\n} from './patterns/unstable-classes';\n\n// ===== Helpers =====\nexport { getStableId } from './helpers/get-stable-id';\nexport { describeRelative } from './helpers/describe-relative';\n\n// ===== Strategies =====\nexport { explicitTrackingAttribute } from './strategies/explicit-tracking-attribute';\nexport { stableId as stableIdStrategy } from './strategies/stable-id';\n\n// ===== Orchestrator + fallback =====\n// Exposed publicly so the dashboard / Chrome extension can call them ad-hoc\n// without instantiating a full engine — useful for preview / debugging flows.\nexport { runOrchestrator, DEFAULT_STRATEGIES } from './orchestrator';\nexport type { OrchestratorOptions } from './orchestrator';\nexport { fallbackCssPath } from './fallback-css-path';\n\n// ===== Config resolver =====\nexport { resolveSelectorConfig, DEFAULT_RESOLVED_CONFIG } from './config/resolve-config';\n\n// ===== Engine factory =====\nexport { createSelectorEngine } from './engine';\nexport type { CreateSelectorEngineOptions } from './engine';\n"]} |
| /** | ||
| * Orchestrator — the entry point for the v1 element-selector algorithm. | ||
| * | ||
| * Given a click target, the orchestrator walks from the target up through its | ||
| * ancestors and tries each registered strategy at each level. The first | ||
| * strategy that returns a non-null candidate (and produces a selector that | ||
| * resolves uniquely under the scope) wins; the trail of intermediate elements | ||
| * between the anchor and the original target is then expressed as a positional | ||
| * descent via `describeRelative`. | ||
| * | ||
| * Two-pass design: every ancestor is tried under the `explicitTrackingAttribute` | ||
| * strategy first, then the entire walk repeats with `stableId`. This ordering | ||
| * means an explicit anchor anywhere up the tree always beats a stable id deeper | ||
| * down — the design doc describes this as the "customer intent overrides | ||
| * structural inference" rule. | ||
| * | ||
| * The orchestrator does not own the fallback. When no strategy + walk produces | ||
| * a unique selector, it returns `null` and the engine wires the | ||
| * `fallback-css-path` path in instead. Keeping the fallback outside the | ||
| * orchestrator keeps the strategy chain testable in isolation. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { ElementSelectorLogger, ResolvedSelectorConfig, Strategy } from './types'; | ||
| /** | ||
| * Default strategy chain in priority order. Used by the engine factory when the | ||
| * caller doesn't provide an explicit list. Exported for diagnostics — tests | ||
| * import this so they don't have to duplicate the list. | ||
| */ | ||
| export declare const DEFAULT_STRATEGIES: ReadonlyArray<Strategy>; | ||
| export interface OrchestratorOptions { | ||
| /** Strategy chain. Defaults to `DEFAULT_STRATEGIES`. Order = priority. */ | ||
| strategies?: ReadonlyArray<Strategy>; | ||
| /** Document or shadow root used for uniqueness checks. Defaults to the target's owner document. */ | ||
| scope?: ParentNode; | ||
| /** | ||
| * Optional logger. When provided, the orchestrator emits a `debug` log if a | ||
| * strategy produces a malformed selector that throws during the uniqueness | ||
| * check — useful for diagnosing strategy bugs without crashing the engine. | ||
| */ | ||
| logger?: ElementSelectorLogger; | ||
| } | ||
| /** | ||
| * Try to produce a CSS selector for `el` using the strategy chain. | ||
| * | ||
| * Returns the composed selector string on success, or `null` if every strategy | ||
| * declined at every walked ancestor. Callers (the engine) treat `null` as the | ||
| * signal to invoke the fallback walker. | ||
| * | ||
| * Selector format on success: | ||
| * | ||
| * - Anchor only (target itself was an anchor): `<anchor-selector>` | ||
| * - Anchor + trail (anchor was an ancestor): `<anchor-selector> > <descent>` | ||
| * | ||
| * Where `<anchor-selector>` is whatever the winning strategy returned for the | ||
| * anchor element (e.g. `[data-amp-track-id="login-form"]` or `div#hero`), and | ||
| * `<descent>` is `describeRelative(anchor, trail)`. | ||
| */ | ||
| export declare function runOrchestrator(el: Element, config: ResolvedSelectorConfig, options?: OrchestratorOptions): string | null; | ||
| //# sourceMappingURL=orchestrator.d.ts.map |
| {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,EAAE,qBAAqB,EAAE,sBAAsB,EAAE,QAAQ,EAAmB,MAAM,SAAS,CAAC;AAKnG;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,EAAE,aAAa,CAAC,QAAQ,CAAiD,CAAC;AAEzG,MAAM,WAAW,mBAAmB;IAClC,0EAA0E;IAC1E,UAAU,CAAC,EAAE,aAAa,CAAC,QAAQ,CAAC,CAAC;IACrC,mGAAmG;IACnG,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,eAAe,CAC7B,EAAE,EAAE,OAAO,EACX,MAAM,EAAE,sBAAsB,EAC9B,OAAO,GAAE,mBAAwB,GAChC,MAAM,GAAG,IAAI,CAoCf"} |
| /** | ||
| * Orchestrator — the entry point for the v1 element-selector algorithm. | ||
| * | ||
| * Given a click target, the orchestrator walks from the target up through its | ||
| * ancestors and tries each registered strategy at each level. The first | ||
| * strategy that returns a non-null candidate (and produces a selector that | ||
| * resolves uniquely under the scope) wins; the trail of intermediate elements | ||
| * between the anchor and the original target is then expressed as a positional | ||
| * descent via `describeRelative`. | ||
| * | ||
| * Two-pass design: every ancestor is tried under the `explicitTrackingAttribute` | ||
| * strategy first, then the entire walk repeats with `stableId`. This ordering | ||
| * means an explicit anchor anywhere up the tree always beats a stable id deeper | ||
| * down — the design doc describes this as the "customer intent overrides | ||
| * structural inference" rule. | ||
| * | ||
| * The orchestrator does not own the fallback. When no strategy + walk produces | ||
| * a unique selector, it returns `null` and the engine wires the | ||
| * `fallback-css-path` path in instead. Keeping the fallback outside the | ||
| * orchestrator keeps the strategy chain testable in isolation. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| import { __values } from "tslib"; | ||
| import { explicitTrackingAttribute } from './strategies/explicit-tracking-attribute'; | ||
| import { stableId as stableIdStrategy } from './strategies/stable-id'; | ||
| import { describeRelative } from './helpers/describe-relative'; | ||
| /** | ||
| * Default strategy chain in priority order. Used by the engine factory when the | ||
| * caller doesn't provide an explicit list. Exported for diagnostics — tests | ||
| * import this so they don't have to duplicate the list. | ||
| */ | ||
| export var DEFAULT_STRATEGIES = [explicitTrackingAttribute, stableIdStrategy]; | ||
| /** | ||
| * Try to produce a CSS selector for `el` using the strategy chain. | ||
| * | ||
| * Returns the composed selector string on success, or `null` if every strategy | ||
| * declined at every walked ancestor. Callers (the engine) treat `null` as the | ||
| * signal to invoke the fallback walker. | ||
| * | ||
| * Selector format on success: | ||
| * | ||
| * - Anchor only (target itself was an anchor): `<anchor-selector>` | ||
| * - Anchor + trail (anchor was an ancestor): `<anchor-selector> > <descent>` | ||
| * | ||
| * Where `<anchor-selector>` is whatever the winning strategy returned for the | ||
| * anchor element (e.g. `[data-amp-track-id="login-form"]` or `div#hero`), and | ||
| * `<descent>` is `describeRelative(anchor, trail)`. | ||
| */ | ||
| export function runOrchestrator(el, config, options) { | ||
| var e_1, _a; | ||
| var _b, _c, _d; | ||
| if (options === void 0) { options = {}; } | ||
| var strategies = (_b = options.strategies) !== null && _b !== void 0 ? _b : DEFAULT_STRATEGIES; | ||
| var scope = (_d = (_c = options.scope) !== null && _c !== void 0 ? _c : el.ownerDocument) !== null && _d !== void 0 ? _d : document; | ||
| var ctx = { scope: scope, config: config }; | ||
| var walk = collectWalk(el, config.maxAncestorWalkDepth); | ||
| try { | ||
| // Two-pass: try strategy[0] across the whole walk, then strategy[1], etc. | ||
| // This gives an "explicit anchor at any depth beats a structural anchor any | ||
| // depth" priority, which matches the design doc's "customer intent overrides | ||
| // structural inference" rule. | ||
| for (var strategies_1 = __values(strategies), strategies_1_1 = strategies_1.next(); !strategies_1_1.done; strategies_1_1 = strategies_1.next()) { | ||
| var strategy = strategies_1_1.value; | ||
| for (var i = 0; i < walk.length; i++) { | ||
| var anchor = walk[i]; | ||
| var anchorSelector = strategy.try(anchor, ctx); | ||
| if (anchorSelector === null) | ||
| continue; | ||
| // Anchor was the target itself — no descent needed. | ||
| if (i === 0) { | ||
| if (isUniqueMatch(scope, anchorSelector, el, strategy.name, options.logger)) { | ||
| return anchorSelector; | ||
| } | ||
| continue; | ||
| } | ||
| // Anchor was an ancestor — descend back to the target through the trail. | ||
| var trail = walk.slice(0, i).reverse(); | ||
| var descent = describeRelative(anchor, trail); | ||
| var composed = "".concat(anchorSelector, " > ").concat(descent); | ||
| if (isUniqueMatch(scope, composed, el, strategy.name, options.logger)) { | ||
| return composed; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
| finally { | ||
| try { | ||
| if (strategies_1_1 && !strategies_1_1.done && (_a = strategies_1.return)) _a.call(strategies_1); | ||
| } | ||
| finally { if (e_1) throw e_1.error; } | ||
| } | ||
| return null; | ||
| } | ||
| /** | ||
| * Build the walk array `[target, parent, grandparent, ...]`, stopping at | ||
| * `<html>` (or at the depth limit if configured). Excludes the document | ||
| * element's parent (which is the Document node and not an Element). | ||
| */ | ||
| function collectWalk(el, maxDepth) { | ||
| var walk = []; | ||
| var cursor = el; | ||
| while (cursor !== null) { | ||
| walk.push(cursor); | ||
| if (maxDepth !== undefined && walk.length > maxDepth) | ||
| break; | ||
| cursor = cursor.parentElement; | ||
| } | ||
| return walk; | ||
| } | ||
| /** | ||
| * Check whether `selector` resolves to exactly `el` and no other element in | ||
| * `scope`. The strategies themselves don't run uniqueness checks — that's the | ||
| * orchestrator's job, so strategies stay pure transforms and stay testable in | ||
| * isolation. | ||
| * | ||
| * When the selector is malformed (which is a strategy bug rather than user | ||
| * input), we log at `debug` and return false rather than throwing. The log | ||
| * lets engineers diagnose a misbehaving strategy without the engine taking | ||
| * down whatever it's wired into. | ||
| */ | ||
| function isUniqueMatch(scope, selector, el, strategyName, logger) { | ||
| var matches; | ||
| try { | ||
| matches = scope.querySelectorAll(selector); | ||
| } | ||
| catch (e) { | ||
| var message = e instanceof Error ? e.message : String(e); | ||
| logger === null || logger === void 0 ? void 0 : logger.debug("@amplitude/element-selector: strategy \"".concat(strategyName, "\" emitted a malformed selector \"").concat(selector, "\" (").concat(message, ")")); | ||
| return false; | ||
| } | ||
| return matches.length === 1 && matches[0] === el; | ||
| } | ||
| //# sourceMappingURL=orchestrator.js.map |
| {"version":3,"file":"orchestrator.js","sourceRoot":"","sources":["../../src/orchestrator.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;;AAGH,OAAO,EAAE,yBAAyB,EAAE,MAAM,0CAA0C,CAAC;AACrF,OAAO,EAAE,QAAQ,IAAI,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,6BAA6B,CAAC;AAE/D;;;;GAIG;AACH,MAAM,CAAC,IAAM,kBAAkB,GAA4B,CAAC,yBAAyB,EAAE,gBAAgB,CAAC,CAAC;AAezG;;;;;;;;;;;;;;;GAeG;AACH,MAAM,UAAU,eAAe,CAC7B,EAAW,EACX,MAA8B,EAC9B,OAAiC;;;IAAjC,wBAAA,EAAA,YAAiC;IAEjC,IAAM,UAAU,GAAG,MAAA,OAAO,CAAC,UAAU,mCAAI,kBAAkB,CAAC;IAC5D,IAAM,KAAK,GAAe,MAAA,MAAA,OAAO,CAAC,KAAK,mCAAI,EAAE,CAAC,aAAa,mCAAI,QAAQ,CAAC;IACxE,IAAM,GAAG,GAAoB,EAAE,KAAK,OAAA,EAAE,MAAM,QAAA,EAAE,CAAC;IAE/C,IAAM,IAAI,GAAG,WAAW,CAAC,EAAE,EAAE,MAAM,CAAC,oBAAoB,CAAC,CAAC;;QAE1D,0EAA0E;QAC1E,4EAA4E;QAC5E,6EAA6E;QAC7E,8BAA8B;QAC9B,KAAuB,IAAA,eAAA,SAAA,UAAU,CAAA,sCAAA,8DAAE;YAA9B,IAAM,QAAQ,uBAAA;YACjB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;gBACpC,IAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;gBACvB,IAAM,cAAc,GAAG,QAAQ,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;gBACjD,IAAI,cAAc,KAAK,IAAI;oBAAE,SAAS;gBAEtC,oDAAoD;gBACpD,IAAI,CAAC,KAAK,CAAC,EAAE;oBACX,IAAI,aAAa,CAAC,KAAK,EAAE,cAAc,EAAE,EAAE,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE;wBAC3E,OAAO,cAAc,CAAC;qBACvB;oBACD,SAAS;iBACV;gBAED,yEAAyE;gBACzE,IAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,EAAE,CAAC;gBACzC,IAAM,OAAO,GAAG,gBAAgB,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;gBAChD,IAAM,QAAQ,GAAG,UAAG,cAAc,gBAAM,OAAO,CAAE,CAAC;gBAClD,IAAI,aAAa,CAAC,KAAK,EAAE,QAAQ,EAAE,EAAE,EAAE,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,MAAM,CAAC,EAAE;oBACrE,OAAO,QAAQ,CAAC;iBACjB;aACF;SACF;;;;;;;;;IAED,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;GAIG;AACH,SAAS,WAAW,CAAC,EAAW,EAAE,QAA4B;IAC5D,IAAM,IAAI,GAAc,EAAE,CAAC;IAC3B,IAAI,MAAM,GAAmB,EAAE,CAAC;IAChC,OAAO,MAAM,KAAK,IAAI,EAAE;QACtB,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAClB,IAAI,QAAQ,KAAK,SAAS,IAAI,IAAI,CAAC,MAAM,GAAG,QAAQ;YAAE,MAAM;QAC5D,MAAM,GAAG,MAAM,CAAC,aAAa,CAAC;KAC/B;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;;;;;;;;GAUG;AACH,SAAS,aAAa,CACpB,KAAiB,EACjB,QAAgB,EAChB,EAAW,EACX,YAAoB,EACpB,MAA8B;IAE9B,IAAI,OAA4B,CAAC;IACjC,IAAI;QACF,OAAO,GAAG,KAAK,CAAC,gBAAgB,CAAC,QAAQ,CAAC,CAAC;KAC5C;IAAC,OAAO,CAAC,EAAE;QACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;QAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,KAAK,CACX,kDAA0C,YAAY,+CAAmC,QAAQ,iBAAM,OAAO,MAAG,CAClH,CAAC;QACF,OAAO,KAAK,CAAC;KACd;IACD,OAAO,OAAO,CAAC,MAAM,KAAK,CAAC,IAAI,OAAO,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC;AACnD,CAAC","sourcesContent":["/**\n * Orchestrator — the entry point for the v1 element-selector algorithm.\n *\n * Given a click target, the orchestrator walks from the target up through its\n * ancestors and tries each registered strategy at each level. The first\n * strategy that returns a non-null candidate (and produces a selector that\n * resolves uniquely under the scope) wins; the trail of intermediate elements\n * between the anchor and the original target is then expressed as a positional\n * descent via `describeRelative`.\n *\n * Two-pass design: every ancestor is tried under the `explicitTrackingAttribute`\n * strategy first, then the entire walk repeats with `stableId`. This ordering\n * means an explicit anchor anywhere up the tree always beats a stable id deeper\n * down — the design doc describes this as the \"customer intent overrides\n * structural inference\" rule.\n *\n * The orchestrator does not own the fallback. When no strategy + walk produces\n * a unique selector, it returns `null` and the engine wires the\n * `fallback-css-path` path in instead. Keeping the fallback outside the\n * orchestrator keeps the strategy chain testable in isolation.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\nimport { ElementSelectorLogger, ResolvedSelectorConfig, Strategy, StrategyContext } from './types';\nimport { explicitTrackingAttribute } from './strategies/explicit-tracking-attribute';\nimport { stableId as stableIdStrategy } from './strategies/stable-id';\nimport { describeRelative } from './helpers/describe-relative';\n\n/**\n * Default strategy chain in priority order. Used by the engine factory when the\n * caller doesn't provide an explicit list. Exported for diagnostics — tests\n * import this so they don't have to duplicate the list.\n */\nexport const DEFAULT_STRATEGIES: ReadonlyArray<Strategy> = [explicitTrackingAttribute, stableIdStrategy];\n\nexport interface OrchestratorOptions {\n /** Strategy chain. Defaults to `DEFAULT_STRATEGIES`. Order = priority. */\n strategies?: ReadonlyArray<Strategy>;\n /** Document or shadow root used for uniqueness checks. Defaults to the target's owner document. */\n scope?: ParentNode;\n /**\n * Optional logger. When provided, the orchestrator emits a `debug` log if a\n * strategy produces a malformed selector that throws during the uniqueness\n * check — useful for diagnosing strategy bugs without crashing the engine.\n */\n logger?: ElementSelectorLogger;\n}\n\n/**\n * Try to produce a CSS selector for `el` using the strategy chain.\n *\n * Returns the composed selector string on success, or `null` if every strategy\n * declined at every walked ancestor. Callers (the engine) treat `null` as the\n * signal to invoke the fallback walker.\n *\n * Selector format on success:\n *\n * - Anchor only (target itself was an anchor): `<anchor-selector>`\n * - Anchor + trail (anchor was an ancestor): `<anchor-selector> > <descent>`\n *\n * Where `<anchor-selector>` is whatever the winning strategy returned for the\n * anchor element (e.g. `[data-amp-track-id=\"login-form\"]` or `div#hero`), and\n * `<descent>` is `describeRelative(anchor, trail)`.\n */\nexport function runOrchestrator(\n el: Element,\n config: ResolvedSelectorConfig,\n options: OrchestratorOptions = {},\n): string | null {\n const strategies = options.strategies ?? DEFAULT_STRATEGIES;\n const scope: ParentNode = options.scope ?? el.ownerDocument ?? document;\n const ctx: StrategyContext = { scope, config };\n\n const walk = collectWalk(el, config.maxAncestorWalkDepth);\n\n // Two-pass: try strategy[0] across the whole walk, then strategy[1], etc.\n // This gives an \"explicit anchor at any depth beats a structural anchor any\n // depth\" priority, which matches the design doc's \"customer intent overrides\n // structural inference\" rule.\n for (const strategy of strategies) {\n for (let i = 0; i < walk.length; i++) {\n const anchor = walk[i];\n const anchorSelector = strategy.try(anchor, ctx);\n if (anchorSelector === null) continue;\n\n // Anchor was the target itself — no descent needed.\n if (i === 0) {\n if (isUniqueMatch(scope, anchorSelector, el, strategy.name, options.logger)) {\n return anchorSelector;\n }\n continue;\n }\n\n // Anchor was an ancestor — descend back to the target through the trail.\n const trail = walk.slice(0, i).reverse();\n const descent = describeRelative(anchor, trail);\n const composed = `${anchorSelector} > ${descent}`;\n if (isUniqueMatch(scope, composed, el, strategy.name, options.logger)) {\n return composed;\n }\n }\n }\n\n return null;\n}\n\n/**\n * Build the walk array `[target, parent, grandparent, ...]`, stopping at\n * `<html>` (or at the depth limit if configured). Excludes the document\n * element's parent (which is the Document node and not an Element).\n */\nfunction collectWalk(el: Element, maxDepth: number | undefined): Element[] {\n const walk: Element[] = [];\n let cursor: Element | null = el;\n while (cursor !== null) {\n walk.push(cursor);\n if (maxDepth !== undefined && walk.length > maxDepth) break;\n cursor = cursor.parentElement;\n }\n return walk;\n}\n\n/**\n * Check whether `selector` resolves to exactly `el` and no other element in\n * `scope`. The strategies themselves don't run uniqueness checks — that's the\n * orchestrator's job, so strategies stay pure transforms and stay testable in\n * isolation.\n *\n * When the selector is malformed (which is a strategy bug rather than user\n * input), we log at `debug` and return false rather than throwing. The log\n * lets engineers diagnose a misbehaving strategy without the engine taking\n * down whatever it's wired into.\n */\nfunction isUniqueMatch(\n scope: ParentNode,\n selector: string,\n el: Element,\n strategyName: string,\n logger?: ElementSelectorLogger,\n): boolean {\n let matches: NodeListOf<Element>;\n try {\n matches = scope.querySelectorAll(selector);\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.debug(\n `@amplitude/element-selector: strategy \"${strategyName}\" emitted a malformed selector \"${selector}\" (${message})`,\n );\n return false;\n }\n return matches.length === 1 && matches[0] === el;\n}\n"]} |
| import { ElementSelectorLogger } from '../types'; | ||
| /** | ||
| * Autogenerated-id pattern pack. | ||
| * | ||
| * When an element id matches any of these regexes, the algorithm treats the | ||
| * element as if it has no id — the stableId strategy returns null and the | ||
| * fallback walks past it instead of anchoring on it. Stops selectors from | ||
| * pinning to ids that change between page loads or component re-mounts. | ||
| * | ||
| * Defaults are surfaced to customers in remote config so they can audit what's | ||
| * being filtered and add or remove patterns to match their stack. | ||
| */ | ||
| /** | ||
| * Built-in defaults, in the order they're applied (order doesn't affect | ||
| * correctness — `some()` short-circuits on the first match). | ||
| */ | ||
| export declare const DEFAULT_AUTOGENERATED_ID_PATTERNS: ReadonlyArray<RegExp>; | ||
| /** | ||
| * Compile a list of regex pattern strings (e.g. from a remote-config payload) | ||
| * into RegExp objects. Invalid patterns are skipped (not thrown) so a single | ||
| * bad regex in remote config can't crash the engine. | ||
| * | ||
| * Pass `logger` to surface invalid patterns at `warn` level — useful for | ||
| * diagnosing why a customer's override isn't taking effect. | ||
| * | ||
| * Callers decide whether to merge the result with `DEFAULT_AUTOGENERATED_ID_PATTERNS` | ||
| * or use it as a full replacement. That policy lives in the config resolver, | ||
| * not here. | ||
| */ | ||
| export declare function compile(patterns: string[], logger?: ElementSelectorLogger): RegExp[]; | ||
| /** | ||
| * Check whether `id` is "stable" relative to a set of patterns. An id is | ||
| * stable when it's a non-empty string AND doesn't match any pattern in | ||
| * `patterns`. Null, undefined, and empty-string ids are not stable. | ||
| */ | ||
| export declare function isStableId(id: string | null | undefined, patterns: ReadonlyArray<RegExp>): boolean; | ||
| //# sourceMappingURL=autogenerated-ids.d.ts.map |
| {"version":3,"file":"autogenerated-ids.d.ts","sourceRoot":"","sources":["../../../src/patterns/autogenerated-ids.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,eAAO,MAAM,iCAAiC,EAAE,aAAa,CAAC,MAAM,CAkBnE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAWpF;AAED;;;;GAIG;AACH,wBAAgB,UAAU,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,OAAO,CAMlG"} |
| import { __values } from "tslib"; | ||
| /** | ||
| * Autogenerated-id pattern pack. | ||
| * | ||
| * When an element id matches any of these regexes, the algorithm treats the | ||
| * element as if it has no id — the stableId strategy returns null and the | ||
| * fallback walks past it instead of anchoring on it. Stops selectors from | ||
| * pinning to ids that change between page loads or component re-mounts. | ||
| * | ||
| * Defaults are surfaced to customers in remote config so they can audit what's | ||
| * being filtered and add or remove patterns to match their stack. | ||
| */ | ||
| /** | ||
| * Built-in defaults, in the order they're applied (order doesn't affect | ||
| * correctness — `some()` short-circuits on the first match). | ||
| */ | ||
| export var DEFAULT_AUTOGENERATED_ID_PATTERNS = [ | ||
| // React useId() — ":r0:", ":r1:", ":rk:". Stable for one render; new id on next mount. | ||
| /^:r[0-9a-z]+:$/, | ||
| // Radix UI primitives — "radix-:r3:", "radix-A1B2C3". Prefix match (the | ||
| // shape of what comes after `radix-` varies by Radix version and React | ||
| // major; only the prefix is reliable). | ||
| /^radix-/, | ||
| // Headless UI (Tailwind Labs) — "headlessui-menu-button-:r5:". Prefix match. | ||
| /^headlessui-/, | ||
| // MUI internal id prefix — "mui-12345". Don't confuse with Mui-* CSS class state markers. | ||
| /^mui-/, | ||
| // Hex-only ids >=16 chars — UUIDs without hyphens, build hashes. Whole-string match. | ||
| /^[a-f0-9]{16,}$/i, | ||
| // Trailing long digit run — "session-1700000000", "row-20250515". Timestamps, counters. | ||
| /-\d{8,}$/, | ||
| // Any 4+ consecutive digits anywhere — "product-2134", "swiper-wrapper-…41039". | ||
| // ContentSquare's heuristic; catches the digit-heavy portions of library-generated ids. | ||
| /\d{4,}/, | ||
| ]; | ||
| /** | ||
| * Compile a list of regex pattern strings (e.g. from a remote-config payload) | ||
| * into RegExp objects. Invalid patterns are skipped (not thrown) so a single | ||
| * bad regex in remote config can't crash the engine. | ||
| * | ||
| * Pass `logger` to surface invalid patterns at `warn` level — useful for | ||
| * diagnosing why a customer's override isn't taking effect. | ||
| * | ||
| * Callers decide whether to merge the result with `DEFAULT_AUTOGENERATED_ID_PATTERNS` | ||
| * or use it as a full replacement. That policy lives in the config resolver, | ||
| * not here. | ||
| */ | ||
| export function compile(patterns, logger) { | ||
| var e_1, _a; | ||
| var compiled = []; | ||
| try { | ||
| for (var patterns_1 = __values(patterns), patterns_1_1 = patterns_1.next(); !patterns_1_1.done; patterns_1_1 = patterns_1.next()) { | ||
| var pattern = patterns_1_1.value; | ||
| try { | ||
| compiled.push(new RegExp(pattern)); | ||
| } | ||
| catch (e) { | ||
| var message = e instanceof Error ? e.message : String(e); | ||
| logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: ignoring invalid autogenerated-id pattern \"".concat(pattern, "\" (").concat(message, ")")); | ||
| } | ||
| } | ||
| } | ||
| catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
| finally { | ||
| try { | ||
| if (patterns_1_1 && !patterns_1_1.done && (_a = patterns_1.return)) _a.call(patterns_1); | ||
| } | ||
| finally { if (e_1) throw e_1.error; } | ||
| } | ||
| return compiled; | ||
| } | ||
| /** | ||
| * Check whether `id` is "stable" relative to a set of patterns. An id is | ||
| * stable when it's a non-empty string AND doesn't match any pattern in | ||
| * `patterns`. Null, undefined, and empty-string ids are not stable. | ||
| */ | ||
| export function isStableId(id, patterns) { | ||
| var e_2, _a; | ||
| if (id === null || id === undefined || id === '') | ||
| return false; | ||
| try { | ||
| for (var patterns_2 = __values(patterns), patterns_2_1 = patterns_2.next(); !patterns_2_1.done; patterns_2_1 = patterns_2.next()) { | ||
| var pattern = patterns_2_1.value; | ||
| if (pattern.test(id)) | ||
| return false; | ||
| } | ||
| } | ||
| catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
| finally { | ||
| try { | ||
| if (patterns_2_1 && !patterns_2_1.done && (_a = patterns_2.return)) _a.call(patterns_2); | ||
| } | ||
| finally { if (e_2) throw e_2.error; } | ||
| } | ||
| return true; | ||
| } | ||
| //# sourceMappingURL=autogenerated-ids.js.map |
| {"version":3,"file":"autogenerated-ids.js","sourceRoot":"","sources":["../../../src/patterns/autogenerated-ids.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;GAUG;AAEH;;;GAGG;AACH,MAAM,CAAC,IAAM,iCAAiC,GAA0B;IACtE,uFAAuF;IACvF,gBAAgB;IAChB,wEAAwE;IACxE,uEAAuE;IACvE,uCAAuC;IACvC,SAAS;IACT,6EAA6E;IAC7E,cAAc;IACd,0FAA0F;IAC1F,OAAO;IACP,qFAAqF;IACrF,kBAAkB;IAClB,wFAAwF;IACxF,UAAU;IACV,gFAAgF;IAChF,wFAAwF;IACxF,QAAQ;CACT,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO,CAAC,QAAkB,EAAE,MAA8B;;IACxE,IAAM,QAAQ,GAAa,EAAE,CAAC;;QAC9B,KAAsB,IAAA,aAAA,SAAA,QAAQ,CAAA,kCAAA,wDAAE;YAA3B,IAAM,OAAO,qBAAA;YAChB,IAAI;gBACF,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;aACpC;YAAC,OAAO,CAAC,EAAE;gBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,mFAA2E,OAAO,iBAAM,OAAO,MAAG,CAAC,CAAC;aAClH;SACF;;;;;;;;;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,EAA6B,EAAE,QAA+B;;IACvF,IAAI,EAAE,KAAK,IAAI,IAAI,EAAE,KAAK,SAAS,IAAI,EAAE,KAAK,EAAE;QAAE,OAAO,KAAK,CAAC;;QAC/D,KAAsB,IAAA,aAAA,SAAA,QAAQ,CAAA,kCAAA,wDAAE;YAA3B,IAAM,OAAO,qBAAA;YAChB,IAAI,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC;gBAAE,OAAO,KAAK,CAAC;SACpC;;;;;;;;;IACD,OAAO,IAAI,CAAC;AACd,CAAC","sourcesContent":["import { ElementSelectorLogger } from '../types';\n\n/**\n * Autogenerated-id pattern pack.\n *\n * When an element id matches any of these regexes, the algorithm treats the\n * element as if it has no id — the stableId strategy returns null and the\n * fallback walks past it instead of anchoring on it. Stops selectors from\n * pinning to ids that change between page loads or component re-mounts.\n *\n * Defaults are surfaced to customers in remote config so they can audit what's\n * being filtered and add or remove patterns to match their stack.\n */\n\n/**\n * Built-in defaults, in the order they're applied (order doesn't affect\n * correctness — `some()` short-circuits on the first match).\n */\nexport const DEFAULT_AUTOGENERATED_ID_PATTERNS: ReadonlyArray<RegExp> = [\n // React useId() — \":r0:\", \":r1:\", \":rk:\". Stable for one render; new id on next mount.\n /^:r[0-9a-z]+:$/,\n // Radix UI primitives — \"radix-:r3:\", \"radix-A1B2C3\". Prefix match (the\n // shape of what comes after `radix-` varies by Radix version and React\n // major; only the prefix is reliable).\n /^radix-/,\n // Headless UI (Tailwind Labs) — \"headlessui-menu-button-:r5:\". Prefix match.\n /^headlessui-/,\n // MUI internal id prefix — \"mui-12345\". Don't confuse with Mui-* CSS class state markers.\n /^mui-/,\n // Hex-only ids >=16 chars — UUIDs without hyphens, build hashes. Whole-string match.\n /^[a-f0-9]{16,}$/i,\n // Trailing long digit run — \"session-1700000000\", \"row-20250515\". Timestamps, counters.\n /-\\d{8,}$/,\n // Any 4+ consecutive digits anywhere — \"product-2134\", \"swiper-wrapper-…41039\".\n // ContentSquare's heuristic; catches the digit-heavy portions of library-generated ids.\n /\\d{4,}/,\n];\n\n/**\n * Compile a list of regex pattern strings (e.g. from a remote-config payload)\n * into RegExp objects. Invalid patterns are skipped (not thrown) so a single\n * bad regex in remote config can't crash the engine.\n *\n * Pass `logger` to surface invalid patterns at `warn` level — useful for\n * diagnosing why a customer's override isn't taking effect.\n *\n * Callers decide whether to merge the result with `DEFAULT_AUTOGENERATED_ID_PATTERNS`\n * or use it as a full replacement. That policy lives in the config resolver,\n * not here.\n */\nexport function compile(patterns: string[], logger?: ElementSelectorLogger): RegExp[] {\n const compiled: RegExp[] = [];\n for (const pattern of patterns) {\n try {\n compiled.push(new RegExp(pattern));\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: ignoring invalid autogenerated-id pattern \"${pattern}\" (${message})`);\n }\n }\n return compiled;\n}\n\n/**\n * Check whether `id` is \"stable\" relative to a set of patterns. An id is\n * stable when it's a non-empty string AND doesn't match any pattern in\n * `patterns`. Null, undefined, and empty-string ids are not stable.\n */\nexport function isStableId(id: string | null | undefined, patterns: ReadonlyArray<RegExp>): boolean {\n if (id === null || id === undefined || id === '') return false;\n for (const pattern of patterns) {\n if (pattern.test(id)) return false;\n }\n return true;\n}\n"]} |
| import { ElementSelectorLogger } from '../types'; | ||
| /** | ||
| * Unstable-class pattern pack. | ||
| * | ||
| * The fallback walker filters classes through these regexes before adding them | ||
| * to a selector. Classes that match are dropped — they don't participate in | ||
| * sibling disambiguation and never appear in the emitted output. Defends | ||
| * against three failure modes: | ||
| * | ||
| * 1. Build-tool / framework utilities (Tailwind, etc.) — class names that | ||
| * look stable but change with every design tweak. | ||
| * 2. CSS-in-JS / build-hash classes (Emotion, CSS modules, styled-components, | ||
| * styled-jsx) — change on every build. | ||
| * 3. Library runtime state classes (Swiper, MUI, Radix, Headless UI, | ||
| * BEM-style is-active/is-open) — class is stable in name but its presence | ||
| * on a given element moves as the user interacts. | ||
| * | ||
| * Defaults are surfaced to customers in remote config so they can audit what's | ||
| * being filtered and add or remove patterns to match their stack. | ||
| */ | ||
| /** | ||
| * Built-in defaults grouped by category for readability. The runtime treats | ||
| * the whole list uniformly via `Array.prototype.some(pattern.test)`. | ||
| */ | ||
| export declare const DEFAULT_UNSTABLE_CLASS_PATTERNS: ReadonlyArray<RegExp>; | ||
| /** | ||
| * Compile a list of regex pattern strings (e.g. from a remote-config payload) | ||
| * into RegExp objects. Invalid patterns are skipped (not thrown) so a single | ||
| * bad regex in remote config can't crash the engine. | ||
| * | ||
| * Pass `logger` to surface invalid patterns at `warn` level — useful for | ||
| * diagnosing why a customer's override isn't taking effect. | ||
| * | ||
| * Callers decide whether to merge the result with `DEFAULT_UNSTABLE_CLASS_PATTERNS` | ||
| * or use it as a full replacement. That policy lives in the config resolver, | ||
| * not here. | ||
| */ | ||
| export declare function compile(patterns: string[], logger?: ElementSelectorLogger): RegExp[]; | ||
| /** | ||
| * Filter a list of class names, dropping any that match a pattern. Returns a | ||
| * new array containing only the survivors, preserving original order. | ||
| * | ||
| * Used by the fallback walker (in the next PR) before adding classes to a | ||
| * selector for sibling disambiguation. Also returns sensibly on null / | ||
| * undefined / empty inputs. | ||
| */ | ||
| export declare function filterClasses(classes: string[], patterns: ReadonlyArray<RegExp>): string[]; | ||
| //# sourceMappingURL=unstable-classes.d.ts.map |
| {"version":3,"file":"unstable-classes.d.ts","sourceRoot":"","sources":["../../../src/patterns/unstable-classes.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,qBAAqB,EAAE,MAAM,UAAU,CAAC;AAEjD;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACH,eAAO,MAAM,+BAA+B,EAAE,aAAa,CAAC,MAAM,CA6CjE,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,wBAAgB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,qBAAqB,GAAG,MAAM,EAAE,CAWpF;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE,QAAQ,EAAE,aAAa,CAAC,MAAM,CAAC,GAAG,MAAM,EAAE,CAe1F"} |
| import { __values } from "tslib"; | ||
| /** | ||
| * Unstable-class pattern pack. | ||
| * | ||
| * The fallback walker filters classes through these regexes before adding them | ||
| * to a selector. Classes that match are dropped — they don't participate in | ||
| * sibling disambiguation and never appear in the emitted output. Defends | ||
| * against three failure modes: | ||
| * | ||
| * 1. Build-tool / framework utilities (Tailwind, etc.) — class names that | ||
| * look stable but change with every design tweak. | ||
| * 2. CSS-in-JS / build-hash classes (Emotion, CSS modules, styled-components, | ||
| * styled-jsx) — change on every build. | ||
| * 3. Library runtime state classes (Swiper, MUI, Radix, Headless UI, | ||
| * BEM-style is-active/is-open) — class is stable in name but its presence | ||
| * on a given element moves as the user interacts. | ||
| * | ||
| * Defaults are surfaced to customers in remote config so they can audit what's | ||
| * being filtered and add or remove patterns to match their stack. | ||
| */ | ||
| /** | ||
| * Built-in defaults grouped by category for readability. The runtime treats | ||
| * the whole list uniformly via `Array.prototype.some(pattern.test)`. | ||
| */ | ||
| export var DEFAULT_UNSTABLE_CLASS_PATTERNS = [ | ||
| // ===== Tailwind / build-tool utilities ===== | ||
| // Tailwind spacing: p-4, px-2, py-8, pt-1, mt-4, etc. | ||
| /^(p|m|px|py|mx|my|pt|pb|pl|pr|mt|mb|ml|mr)-\d+$/, | ||
| // Tailwind sizing: w-full, h-screen, max-w-[1440px], etc. | ||
| /^(w|h|min-w|max-w|min-h|max-h)-/, | ||
| // Tailwind color / visual: bg-blue-500, text-white, border-gray-200, ring-2, etc. | ||
| /^(text|bg|border|ring|fill|stroke)-/, | ||
| // Tailwind state variants: hover:underline, focus:ring-2, active:bg-transparent. | ||
| /^(hover|focus|active|disabled|group-hover):/, | ||
| // Tailwind breakpoint variants: md:flex, lg:grid-cols-3. | ||
| /^(sm|md|lg|xl|2xl):/, | ||
| // Tailwind z-index utility: z-10, z-50. | ||
| /^z-\d+$/, | ||
| // Tailwind arbitrary data-attribute variants: data-[state=open]:bg-white. | ||
| /^data-\[/, | ||
| // Tailwind arbitrary selector variants: [&_.swiper-slide]:h-auto, [&>div]:p-4. | ||
| /^\[/, | ||
| // ===== CSS-in-JS / build hashes ===== | ||
| // Emotion: css-1abcd23, css-9xyzkw0. | ||
| /^css-[a-z0-9]{6,}$/, | ||
| // CSS modules: Button_root__abc123, Card_container__xyz789. The middle | ||
| // segment can be as short as `root` (4 chars) in practice; the trailing | ||
| // hash is the load-bearing part. | ||
| /^[a-zA-Z]+_[a-zA-Z0-9]{3,}__[a-zA-Z0-9]{5,}$/, | ||
| // styled-components: sc-bdVaJa, sc-1jjuPXC0. | ||
| /^sc-[a-zA-Z0-9]{6,}$/, | ||
| // styled-jsx (Next.js): jsx-1234567. | ||
| /^jsx-\d+$/, | ||
| // ===== Library runtime state classes ===== | ||
| // Swiper carousel slide states. Move between elements as carousel advances. | ||
| /^swiper-slide-(visible|fully-visible|active|prev|next|duplicate)$/, | ||
| // BEM-style interaction state: is-active, is-open, is-selected, etc. | ||
| /^is-(active|open|selected|hovered|focused|expanded)$/, | ||
| // MUI per-component state: MuiButton-focusVisible, MuiSwitch-checked, etc. | ||
| /^Mui[A-Z][a-zA-Z]+-(focused|selected|disabled|expanded|focusVisible|active|checked)$/, | ||
| // MUI bare state classes: Mui-selected, Mui-disabled. | ||
| /^Mui-(selected|focused|disabled|expanded|focusVisible|active|checked)$/, | ||
| // Radix-style state class mirrors: data-state-open, data-state-checked. | ||
| /^data-state-/, | ||
| ]; | ||
| /** | ||
| * Compile a list of regex pattern strings (e.g. from a remote-config payload) | ||
| * into RegExp objects. Invalid patterns are skipped (not thrown) so a single | ||
| * bad regex in remote config can't crash the engine. | ||
| * | ||
| * Pass `logger` to surface invalid patterns at `warn` level — useful for | ||
| * diagnosing why a customer's override isn't taking effect. | ||
| * | ||
| * Callers decide whether to merge the result with `DEFAULT_UNSTABLE_CLASS_PATTERNS` | ||
| * or use it as a full replacement. That policy lives in the config resolver, | ||
| * not here. | ||
| */ | ||
| export function compile(patterns, logger) { | ||
| var e_1, _a; | ||
| var compiled = []; | ||
| try { | ||
| for (var patterns_1 = __values(patterns), patterns_1_1 = patterns_1.next(); !patterns_1_1.done; patterns_1_1 = patterns_1.next()) { | ||
| var pattern = patterns_1_1.value; | ||
| try { | ||
| compiled.push(new RegExp(pattern)); | ||
| } | ||
| catch (e) { | ||
| var message = e instanceof Error ? e.message : String(e); | ||
| logger === null || logger === void 0 ? void 0 : logger.warn("@amplitude/element-selector: ignoring invalid unstable-class pattern \"".concat(pattern, "\" (").concat(message, ")")); | ||
| } | ||
| } | ||
| } | ||
| catch (e_1_1) { e_1 = { error: e_1_1 }; } | ||
| finally { | ||
| try { | ||
| if (patterns_1_1 && !patterns_1_1.done && (_a = patterns_1.return)) _a.call(patterns_1); | ||
| } | ||
| finally { if (e_1) throw e_1.error; } | ||
| } | ||
| return compiled; | ||
| } | ||
| /** | ||
| * Filter a list of class names, dropping any that match a pattern. Returns a | ||
| * new array containing only the survivors, preserving original order. | ||
| * | ||
| * Used by the fallback walker (in the next PR) before adding classes to a | ||
| * selector for sibling disambiguation. Also returns sensibly on null / | ||
| * undefined / empty inputs. | ||
| */ | ||
| export function filterClasses(classes, patterns) { | ||
| var e_2, _a, e_3, _b; | ||
| if (!classes || classes.length === 0) | ||
| return []; | ||
| var survivors = []; | ||
| try { | ||
| for (var classes_1 = __values(classes), classes_1_1 = classes_1.next(); !classes_1_1.done; classes_1_1 = classes_1.next()) { | ||
| var cls = classes_1_1.value; | ||
| if (!cls) | ||
| continue; | ||
| var matched = false; | ||
| try { | ||
| for (var patterns_2 = (e_3 = void 0, __values(patterns)), patterns_2_1 = patterns_2.next(); !patterns_2_1.done; patterns_2_1 = patterns_2.next()) { | ||
| var pattern = patterns_2_1.value; | ||
| if (pattern.test(cls)) { | ||
| matched = true; | ||
| break; | ||
| } | ||
| } | ||
| } | ||
| catch (e_3_1) { e_3 = { error: e_3_1 }; } | ||
| finally { | ||
| try { | ||
| if (patterns_2_1 && !patterns_2_1.done && (_b = patterns_2.return)) _b.call(patterns_2); | ||
| } | ||
| finally { if (e_3) throw e_3.error; } | ||
| } | ||
| if (!matched) | ||
| survivors.push(cls); | ||
| } | ||
| } | ||
| catch (e_2_1) { e_2 = { error: e_2_1 }; } | ||
| finally { | ||
| try { | ||
| if (classes_1_1 && !classes_1_1.done && (_a = classes_1.return)) _a.call(classes_1); | ||
| } | ||
| finally { if (e_2) throw e_2.error; } | ||
| } | ||
| return survivors; | ||
| } | ||
| //# sourceMappingURL=unstable-classes.js.map |
| {"version":3,"file":"unstable-classes.js","sourceRoot":"","sources":["../../../src/patterns/unstable-classes.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;GAkBG;AAEH;;;GAGG;AACH,MAAM,CAAC,IAAM,+BAA+B,GAA0B;IACpE,8CAA8C;IAE9C,sDAAsD;IACtD,iDAAiD;IACjD,0DAA0D;IAC1D,iCAAiC;IACjC,kFAAkF;IAClF,qCAAqC;IACrC,iFAAiF;IACjF,6CAA6C;IAC7C,yDAAyD;IACzD,qBAAqB;IACrB,wCAAwC;IACxC,SAAS;IACT,0EAA0E;IAC1E,UAAU;IACV,+EAA+E;IAC/E,KAAK;IAEL,uCAAuC;IAEvC,qCAAqC;IACrC,oBAAoB;IACpB,uEAAuE;IACvE,wEAAwE;IACxE,iCAAiC;IACjC,8CAA8C;IAC9C,6CAA6C;IAC7C,sBAAsB;IACtB,qCAAqC;IACrC,WAAW;IAEX,4CAA4C;IAE5C,4EAA4E;IAC5E,mEAAmE;IACnE,qEAAqE;IACrE,sDAAsD;IACtD,2EAA2E;IAC3E,sFAAsF;IACtF,sDAAsD;IACtD,wEAAwE;IACxE,wEAAwE;IACxE,cAAc;CACf,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,OAAO,CAAC,QAAkB,EAAE,MAA8B;;IACxE,IAAM,QAAQ,GAAa,EAAE,CAAC;;QAC9B,KAAsB,IAAA,aAAA,SAAA,QAAQ,CAAA,kCAAA,wDAAE;YAA3B,IAAM,OAAO,qBAAA;YAChB,IAAI;gBACF,QAAQ,CAAC,IAAI,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;aACpC;YAAC,OAAO,CAAC,EAAE;gBACV,IAAM,OAAO,GAAG,CAAC,YAAY,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC3D,MAAM,aAAN,MAAM,uBAAN,MAAM,CAAE,IAAI,CAAC,iFAAyE,OAAO,iBAAM,OAAO,MAAG,CAAC,CAAC;aAChH;SACF;;;;;;;;;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,aAAa,CAAC,OAAiB,EAAE,QAA+B;;IAC9E,IAAI,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAChD,IAAM,SAAS,GAAa,EAAE,CAAC;;QAC/B,KAAkB,IAAA,YAAA,SAAA,OAAO,CAAA,gCAAA,qDAAE;YAAtB,IAAM,GAAG,oBAAA;YACZ,IAAI,CAAC,GAAG;gBAAE,SAAS;YACnB,IAAI,OAAO,GAAG,KAAK,CAAC;;gBACpB,KAAsB,IAAA,4BAAA,SAAA,QAAQ,CAAA,CAAA,kCAAA,wDAAE;oBAA3B,IAAM,OAAO,qBAAA;oBAChB,IAAI,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE;wBACrB,OAAO,GAAG,IAAI,CAAC;wBACf,MAAM;qBACP;iBACF;;;;;;;;;YACD,IAAI,CAAC,OAAO;gBAAE,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;SACnC;;;;;;;;;IACD,OAAO,SAAS,CAAC;AACnB,CAAC","sourcesContent":["import { ElementSelectorLogger } from '../types';\n\n/**\n * Unstable-class pattern pack.\n *\n * The fallback walker filters classes through these regexes before adding them\n * to a selector. Classes that match are dropped — they don't participate in\n * sibling disambiguation and never appear in the emitted output. Defends\n * against three failure modes:\n *\n * 1. Build-tool / framework utilities (Tailwind, etc.) — class names that\n * look stable but change with every design tweak.\n * 2. CSS-in-JS / build-hash classes (Emotion, CSS modules, styled-components,\n * styled-jsx) — change on every build.\n * 3. Library runtime state classes (Swiper, MUI, Radix, Headless UI,\n * BEM-style is-active/is-open) — class is stable in name but its presence\n * on a given element moves as the user interacts.\n *\n * Defaults are surfaced to customers in remote config so they can audit what's\n * being filtered and add or remove patterns to match their stack.\n */\n\n/**\n * Built-in defaults grouped by category for readability. The runtime treats\n * the whole list uniformly via `Array.prototype.some(pattern.test)`.\n */\nexport const DEFAULT_UNSTABLE_CLASS_PATTERNS: ReadonlyArray<RegExp> = [\n // ===== Tailwind / build-tool utilities =====\n\n // Tailwind spacing: p-4, px-2, py-8, pt-1, mt-4, etc.\n /^(p|m|px|py|mx|my|pt|pb|pl|pr|mt|mb|ml|mr)-\\d+$/,\n // Tailwind sizing: w-full, h-screen, max-w-[1440px], etc.\n /^(w|h|min-w|max-w|min-h|max-h)-/,\n // Tailwind color / visual: bg-blue-500, text-white, border-gray-200, ring-2, etc.\n /^(text|bg|border|ring|fill|stroke)-/,\n // Tailwind state variants: hover:underline, focus:ring-2, active:bg-transparent.\n /^(hover|focus|active|disabled|group-hover):/,\n // Tailwind breakpoint variants: md:flex, lg:grid-cols-3.\n /^(sm|md|lg|xl|2xl):/,\n // Tailwind z-index utility: z-10, z-50.\n /^z-\\d+$/,\n // Tailwind arbitrary data-attribute variants: data-[state=open]:bg-white.\n /^data-\\[/,\n // Tailwind arbitrary selector variants: [&_.swiper-slide]:h-auto, [&>div]:p-4.\n /^\\[/,\n\n // ===== CSS-in-JS / build hashes =====\n\n // Emotion: css-1abcd23, css-9xyzkw0.\n /^css-[a-z0-9]{6,}$/,\n // CSS modules: Button_root__abc123, Card_container__xyz789. The middle\n // segment can be as short as `root` (4 chars) in practice; the trailing\n // hash is the load-bearing part.\n /^[a-zA-Z]+_[a-zA-Z0-9]{3,}__[a-zA-Z0-9]{5,}$/,\n // styled-components: sc-bdVaJa, sc-1jjuPXC0.\n /^sc-[a-zA-Z0-9]{6,}$/,\n // styled-jsx (Next.js): jsx-1234567.\n /^jsx-\\d+$/,\n\n // ===== Library runtime state classes =====\n\n // Swiper carousel slide states. Move between elements as carousel advances.\n /^swiper-slide-(visible|fully-visible|active|prev|next|duplicate)$/,\n // BEM-style interaction state: is-active, is-open, is-selected, etc.\n /^is-(active|open|selected|hovered|focused|expanded)$/,\n // MUI per-component state: MuiButton-focusVisible, MuiSwitch-checked, etc.\n /^Mui[A-Z][a-zA-Z]+-(focused|selected|disabled|expanded|focusVisible|active|checked)$/,\n // MUI bare state classes: Mui-selected, Mui-disabled.\n /^Mui-(selected|focused|disabled|expanded|focusVisible|active|checked)$/,\n // Radix-style state class mirrors: data-state-open, data-state-checked.\n /^data-state-/,\n];\n\n/**\n * Compile a list of regex pattern strings (e.g. from a remote-config payload)\n * into RegExp objects. Invalid patterns are skipped (not thrown) so a single\n * bad regex in remote config can't crash the engine.\n *\n * Pass `logger` to surface invalid patterns at `warn` level — useful for\n * diagnosing why a customer's override isn't taking effect.\n *\n * Callers decide whether to merge the result with `DEFAULT_UNSTABLE_CLASS_PATTERNS`\n * or use it as a full replacement. That policy lives in the config resolver,\n * not here.\n */\nexport function compile(patterns: string[], logger?: ElementSelectorLogger): RegExp[] {\n const compiled: RegExp[] = [];\n for (const pattern of patterns) {\n try {\n compiled.push(new RegExp(pattern));\n } catch (e) {\n const message = e instanceof Error ? e.message : String(e);\n logger?.warn(`@amplitude/element-selector: ignoring invalid unstable-class pattern \"${pattern}\" (${message})`);\n }\n }\n return compiled;\n}\n\n/**\n * Filter a list of class names, dropping any that match a pattern. Returns a\n * new array containing only the survivors, preserving original order.\n *\n * Used by the fallback walker (in the next PR) before adding classes to a\n * selector for sibling disambiguation. Also returns sensibly on null /\n * undefined / empty inputs.\n */\nexport function filterClasses(classes: string[], patterns: ReadonlyArray<RegExp>): string[] {\n if (!classes || classes.length === 0) return [];\n const survivors: string[] = [];\n for (const cls of classes) {\n if (!cls) continue;\n let matched = false;\n for (const pattern of patterns) {\n if (pattern.test(cls)) {\n matched = true;\n break;\n }\n }\n if (!matched) survivors.push(cls);\n }\n return survivors;\n}\n"]} |
| import { Strategy } from '../types'; | ||
| /** | ||
| * First strategy in the chain. Customer-controlled anchor selector via the | ||
| * configured tracking attribute (default: `data-amp-track-id`). | ||
| * | ||
| * Semantics: | ||
| * | ||
| * - Attribute set with a non-empty value: | ||
| * returns `[<attr>="<value>"]` — uses this element as an explicit anchor. | ||
| * - Attribute set with an empty value: | ||
| * returns null. The empty value is a suppression signal for downstream | ||
| * components (the `stableId` strategy and the fallback both consult | ||
| * `getStableId` which honors the empty-value semantic). This strategy | ||
| * doesn't anchor on the element either. | ||
| * - Attribute absent: | ||
| * returns null. | ||
| * | ||
| * The selector wraps the value in JSON-encoded quotes so any special CSS | ||
| * characters in the value are escaped uniformly. | ||
| */ | ||
| export declare const explicitTrackingAttribute: Strategy; | ||
| //# sourceMappingURL=explicit-tracking-attribute.d.ts.map |
| {"version":3,"file":"explicit-tracking-attribute.d.ts","sourceRoot":"","sources":["../../../src/strategies/explicit-tracking-attribute.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAEpC;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,yBAAyB,EAAE,QAavC,CAAC"} |
| /** | ||
| * First strategy in the chain. Customer-controlled anchor selector via the | ||
| * configured tracking attribute (default: `data-amp-track-id`). | ||
| * | ||
| * Semantics: | ||
| * | ||
| * - Attribute set with a non-empty value: | ||
| * returns `[<attr>="<value>"]` — uses this element as an explicit anchor. | ||
| * - Attribute set with an empty value: | ||
| * returns null. The empty value is a suppression signal for downstream | ||
| * components (the `stableId` strategy and the fallback both consult | ||
| * `getStableId` which honors the empty-value semantic). This strategy | ||
| * doesn't anchor on the element either. | ||
| * - Attribute absent: | ||
| * returns null. | ||
| * | ||
| * The selector wraps the value in JSON-encoded quotes so any special CSS | ||
| * characters in the value are escaped uniformly. | ||
| */ | ||
| export var explicitTrackingAttribute = { | ||
| name: 'explicitTrackingAttribute', | ||
| try: function (el, ctx) { | ||
| var attrName = ctx.config.explicitTrackingAttribute; | ||
| var value = el.getAttribute(attrName); | ||
| if (value === null || value === '') { | ||
| return null; | ||
| } | ||
| // JSON.stringify wraps in double quotes and escapes embedded quotes / | ||
| // backslashes — the resulting attribute selector is valid CSS. | ||
| return "[".concat(attrName, "=").concat(JSON.stringify(value), "]"); | ||
| }, | ||
| }; | ||
| //# sourceMappingURL=explicit-tracking-attribute.js.map |
| {"version":3,"file":"explicit-tracking-attribute.js","sourceRoot":"","sources":["../../../src/strategies/explicit-tracking-attribute.ts"],"names":[],"mappings":"AAEA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,IAAM,yBAAyB,GAAa;IACjD,IAAI,EAAE,2BAA2B;IAEjC,GAAG,YAAC,EAAE,EAAE,GAAG;QACT,IAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,yBAAyB,CAAC;QACtD,IAAM,KAAK,GAAG,EAAE,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QACxC,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,KAAK,EAAE,EAAE;YAClC,OAAO,IAAI,CAAC;SACb;QACD,sEAAsE;QACtE,+DAA+D;QAC/D,OAAO,WAAI,QAAQ,cAAI,IAAI,CAAC,SAAS,CAAC,KAAK,CAAC,MAAG,CAAC;IAClD,CAAC;CACF,CAAC","sourcesContent":["import { Strategy } from '../types';\n\n/**\n * First strategy in the chain. Customer-controlled anchor selector via the\n * configured tracking attribute (default: `data-amp-track-id`).\n *\n * Semantics:\n *\n * - Attribute set with a non-empty value:\n * returns `[<attr>=\"<value>\"]` — uses this element as an explicit anchor.\n * - Attribute set with an empty value:\n * returns null. The empty value is a suppression signal for downstream\n * components (the `stableId` strategy and the fallback both consult\n * `getStableId` which honors the empty-value semantic). This strategy\n * doesn't anchor on the element either.\n * - Attribute absent:\n * returns null.\n *\n * The selector wraps the value in JSON-encoded quotes so any special CSS\n * characters in the value are escaped uniformly.\n */\nexport const explicitTrackingAttribute: Strategy = {\n name: 'explicitTrackingAttribute',\n\n try(el, ctx) {\n const attrName = ctx.config.explicitTrackingAttribute;\n const value = el.getAttribute(attrName);\n if (value === null || value === '') {\n return null;\n }\n // JSON.stringify wraps in double quotes and escapes embedded quotes /\n // backslashes — the resulting attribute selector is valid CSS.\n return `[${attrName}=${JSON.stringify(value)}]`;\n },\n};\n"]} |
| import { Strategy } from '../types'; | ||
| /** | ||
| * Second strategy in the chain. Anchors on the element's `id` when the id is | ||
| * "stable" — i.e., present, not suppressed via the explicit-tracking-attribute | ||
| * empty-value mechanism, and not matching any autogenerated-id pattern. | ||
| * | ||
| * Returns `tag#id` (CSS-escaped) when the id is usable; null otherwise. | ||
| * | ||
| * Filtering is delegated to `getStableId` so this strategy and the | ||
| * `fallback-css-path` walker (in the orchestration PR) share a single source | ||
| * of truth for "is this id usable?" | ||
| */ | ||
| export declare const stableId: Strategy; | ||
| //# sourceMappingURL=stable-id.d.ts.map |
| {"version":3,"file":"stable-id.d.ts","sourceRoot":"","sources":["../../../src/strategies/stable-id.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,UAAU,CAAC;AAIpC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,QAAQ,EAAE,QAUtB,CAAC"} |
| import { getStableId } from '../helpers/get-stable-id'; | ||
| import { escapeIdForCss } from '../helpers/escape-id'; | ||
| /** | ||
| * Second strategy in the chain. Anchors on the element's `id` when the id is | ||
| * "stable" — i.e., present, not suppressed via the explicit-tracking-attribute | ||
| * empty-value mechanism, and not matching any autogenerated-id pattern. | ||
| * | ||
| * Returns `tag#id` (CSS-escaped) when the id is usable; null otherwise. | ||
| * | ||
| * Filtering is delegated to `getStableId` so this strategy and the | ||
| * `fallback-css-path` walker (in the orchestration PR) share a single source | ||
| * of truth for "is this id usable?" | ||
| */ | ||
| export var stableId = { | ||
| name: 'stableId', | ||
| try: function (el, ctx) { | ||
| var id = getStableId(el, ctx.config); | ||
| if (id === null) { | ||
| return null; | ||
| } | ||
| return "".concat(el.tagName.toLowerCase(), "#").concat(escapeIdForCss(id)); | ||
| }, | ||
| }; | ||
| //# sourceMappingURL=stable-id.js.map |
| {"version":3,"file":"stable-id.js","sourceRoot":"","sources":["../../../src/strategies/stable-id.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD;;;;;;;;;;GAUG;AACH,MAAM,CAAC,IAAM,QAAQ,GAAa;IAChC,IAAI,EAAE,UAAU;IAEhB,GAAG,YAAC,EAAE,EAAE,GAAG;QACT,IAAM,EAAE,GAAG,WAAW,CAAC,EAAE,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;QACvC,IAAI,EAAE,KAAK,IAAI,EAAE;YACf,OAAO,IAAI,CAAC;SACb;QACD,OAAO,UAAG,EAAE,CAAC,OAAO,CAAC,WAAW,EAAE,cAAI,cAAc,CAAC,EAAE,CAAC,CAAE,CAAC;IAC7D,CAAC;CACF,CAAC","sourcesContent":["import { Strategy } from '../types';\nimport { getStableId } from '../helpers/get-stable-id';\nimport { escapeIdForCss } from '../helpers/escape-id';\n\n/**\n * Second strategy in the chain. Anchors on the element's `id` when the id is\n * \"stable\" — i.e., present, not suppressed via the explicit-tracking-attribute\n * empty-value mechanism, and not matching any autogenerated-id pattern.\n *\n * Returns `tag#id` (CSS-escaped) when the id is usable; null otherwise.\n *\n * Filtering is delegated to `getStableId` so this strategy and the\n * `fallback-css-path` walker (in the orchestration PR) share a single source\n * of truth for \"is this id usable?\"\n */\nexport const stableId: Strategy = {\n name: 'stableId',\n\n try(el, ctx) {\n const id = getStableId(el, ctx.config);\n if (id === null) {\n return null;\n }\n return `${el.tagName.toLowerCase()}#${escapeIdForCss(id)}`;\n },\n};\n"]} |
| /** | ||
| * Core type definitions for the v1 element-selector algorithm. | ||
| * | ||
| * These are the public-API contract that consumers (autocapture SDK, dashboard | ||
| * tagging UI, Chrome extension visual tagger) design against. They land before | ||
| * any implementation so the contract is stable for cross-team review. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| /** | ||
| * A pluggable selector strategy. Given an element + context, returns a CSS | ||
| * selector string candidate (which the orchestrator will then uniqueness-check) | ||
| * or `null` when the strategy doesn't apply to this element. | ||
| */ | ||
| export interface Strategy { | ||
| /** Stable identifier for the strategy. Used for diagnostics and remote-config references. */ | ||
| readonly name: string; | ||
| /** | ||
| * Produce a candidate selector for `el`, or `null` if this strategy has | ||
| * nothing to contribute for this element. Strategies are pure transforms — | ||
| * they never call `querySelectorAll` themselves; uniqueness is checked by | ||
| * the orchestrator in the next layer up. | ||
| */ | ||
| try(el: Element, ctx: StrategyContext): string | null; | ||
| } | ||
| /** | ||
| * Context passed to every strategy invocation. `scope` is the document or | ||
| * shadow root the orchestrator will use for uniqueness checks; `config` is the | ||
| * resolved configuration (defaults + remote + local options merged). | ||
| */ | ||
| export interface StrategyContext { | ||
| scope: ParentNode; | ||
| config: ResolvedSelectorConfig; | ||
| } | ||
| /** | ||
| * Logger shape accepted by element-selector diagnostics. | ||
| * | ||
| * This is intentionally structural so callers can pass the SDK's | ||
| * `@amplitude/analytics-core` ILogger without this package needing to import | ||
| * analytics-core types during standalone tests/builds. | ||
| */ | ||
| export interface ElementSelectorLogger { | ||
| warn(...args: unknown[]): void; | ||
| debug(...args: unknown[]): void; | ||
| } | ||
| /** | ||
| * Fully-resolved configuration consumed at runtime. Produced by | ||
| * `resolveSelectorConfig` (lands in the orchestration PR) from the | ||
| * `ElementSelectorRemoteConfig` payload plus built-in defaults. | ||
| */ | ||
| export interface ResolvedSelectorConfig { | ||
| /** Master kill switch. When false, autocapture reverts to legacy cssPath. */ | ||
| enabled: boolean; | ||
| /** Attribute name customers can use to set or suppress anchors. Default: 'data-amp-track-id'. */ | ||
| explicitTrackingAttribute: string; | ||
| /** Compiled regex patterns matching ids the algorithm should treat as autogenerated. */ | ||
| autogeneratedIdPatterns: RegExp[]; | ||
| /** Compiled regex patterns matching classes the fallback should filter from sibling disambiguation. */ | ||
| unstableClassPatterns: RegExp[]; | ||
| /** Optional throttle on the ancestor walk. When undefined, the walk runs to <html>. */ | ||
| maxAncestorWalkDepth?: number; | ||
| } | ||
| /** | ||
| * Remote-config payload shape. Each field is optional; absent fields fall back | ||
| * to built-in defaults. Customers see the defaults in the remote-config UI and | ||
| * may add or remove patterns as needed. | ||
| */ | ||
| export interface ElementSelectorRemoteConfig { | ||
| /** Kill switch. When set, overrides the default at resolve time. */ | ||
| enabled?: boolean; | ||
| /** Override the attribute name used for explicit tracking. */ | ||
| explicitTrackingAttribute?: string; | ||
| /** Full replacement for the autogenerated-id pattern list. When omitted, defaults apply. */ | ||
| autogeneratedIdPatterns?: string[]; | ||
| /** Full replacement for the unstable-class pattern list. When omitted, defaults apply. */ | ||
| unstableClassPatterns?: string[]; | ||
| /** Defensive throttle on the ancestor walk. Omit for unbounded walking to <html>. */ | ||
| maxAncestorWalkDepth?: number; | ||
| } | ||
| /** | ||
| * Public interface of the runtime engine. Implementation lands in the | ||
| * orchestration PR; the interface is declared here so consumers can write | ||
| * against the contract before the class exists. | ||
| */ | ||
| export interface SelectorEngine { | ||
| /** Produce a CSS selector string identifying `el` using the engine's current config. */ | ||
| generate(el: Element): string; | ||
| /** Read-only access to the current resolved config — for diagnostics and consumer hydration. */ | ||
| getConfig(): Readonly<ResolvedSelectorConfig>; | ||
| /** Replace the current config. Notifies any onConfigChange subscribers. */ | ||
| updateConfig(next: ResolvedSelectorConfig): void; | ||
| /** | ||
| * Subscribe to config-change notifications. Returns an unsubscribe function. | ||
| * Used by the Chrome extension to keep its locally-hydrated engine in sync | ||
| * with the customer's SDK as remote config updates flow in. | ||
| */ | ||
| onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void; | ||
| } | ||
| //# sourceMappingURL=types.d.ts.map |
| {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH;;;;GAIG;AACH,MAAM,WAAW,QAAQ;IACvB,6FAA6F;IAC7F,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAEtB;;;;;OAKG;IACH,GAAG,CAAC,EAAE,EAAE,OAAO,EAAE,GAAG,EAAE,eAAe,GAAG,MAAM,GAAG,IAAI,CAAC;CACvD;AAED;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,UAAU,CAAC;IAClB,MAAM,EAAE,sBAAsB,CAAC;CAChC;AAED;;;;;;GAMG;AACH,MAAM,WAAW,qBAAqB;IACpC,IAAI,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;CACjC;AAED;;;;GAIG;AACH,MAAM,WAAW,sBAAsB;IACrC,6EAA6E;IAC7E,OAAO,EAAE,OAAO,CAAC;IACjB,iGAAiG;IACjG,yBAAyB,EAAE,MAAM,CAAC;IAClC,wFAAwF;IACxF,uBAAuB,EAAE,MAAM,EAAE,CAAC;IAClC,uGAAuG;IACvG,qBAAqB,EAAE,MAAM,EAAE,CAAC;IAChC,uFAAuF;IACvF,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,2BAA2B;IAC1C,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,8DAA8D;IAC9D,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,4FAA4F;IAC5F,uBAAuB,CAAC,EAAE,MAAM,EAAE,CAAC;IACnC,0FAA0F;IAC1F,qBAAqB,CAAC,EAAE,MAAM,EAAE,CAAC;IACjC,qFAAqF;IACrF,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B,wFAAwF;IACxF,QAAQ,CAAC,EAAE,EAAE,OAAO,GAAG,MAAM,CAAC;IAE9B,gGAAgG;IAChG,SAAS,IAAI,QAAQ,CAAC,sBAAsB,CAAC,CAAC;IAE9C,2EAA2E;IAC3E,YAAY,CAAC,IAAI,EAAE,sBAAsB,GAAG,IAAI,CAAC;IAEjD;;;;OAIG;IACH,cAAc,CAAC,EAAE,EAAE,CAAC,MAAM,EAAE,sBAAsB,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAC1E"} |
| /** | ||
| * Core type definitions for the v1 element-selector algorithm. | ||
| * | ||
| * These are the public-API contract that consumers (autocapture SDK, dashboard | ||
| * tagging UI, Chrome extension visual tagger) design against. They land before | ||
| * any implementation so the contract is stable for cross-team review. | ||
| * | ||
| * See the design doc: | ||
| * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md | ||
| */ | ||
| export {}; | ||
| //# sourceMappingURL=types.js.map |
| {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG","sourcesContent":["/**\n * Core type definitions for the v1 element-selector algorithm.\n *\n * These are the public-API contract that consumers (autocapture SDK, dashboard\n * tagging UI, Chrome extension visual tagger) design against. They land before\n * any implementation so the contract is stable for cross-team review.\n *\n * See the design doc:\n * packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md\n */\n\n/**\n * A pluggable selector strategy. Given an element + context, returns a CSS\n * selector string candidate (which the orchestrator will then uniqueness-check)\n * or `null` when the strategy doesn't apply to this element.\n */\nexport interface Strategy {\n /** Stable identifier for the strategy. Used for diagnostics and remote-config references. */\n readonly name: string;\n\n /**\n * Produce a candidate selector for `el`, or `null` if this strategy has\n * nothing to contribute for this element. Strategies are pure transforms —\n * they never call `querySelectorAll` themselves; uniqueness is checked by\n * the orchestrator in the next layer up.\n */\n try(el: Element, ctx: StrategyContext): string | null;\n}\n\n/**\n * Context passed to every strategy invocation. `scope` is the document or\n * shadow root the orchestrator will use for uniqueness checks; `config` is the\n * resolved configuration (defaults + remote + local options merged).\n */\nexport interface StrategyContext {\n scope: ParentNode;\n config: ResolvedSelectorConfig;\n}\n\n/**\n * Logger shape accepted by element-selector diagnostics.\n *\n * This is intentionally structural so callers can pass the SDK's\n * `@amplitude/analytics-core` ILogger without this package needing to import\n * analytics-core types during standalone tests/builds.\n */\nexport interface ElementSelectorLogger {\n warn(...args: unknown[]): void;\n debug(...args: unknown[]): void;\n}\n\n/**\n * Fully-resolved configuration consumed at runtime. Produced by\n * `resolveSelectorConfig` (lands in the orchestration PR) from the\n * `ElementSelectorRemoteConfig` payload plus built-in defaults.\n */\nexport interface ResolvedSelectorConfig {\n /** Master kill switch. When false, autocapture reverts to legacy cssPath. */\n enabled: boolean;\n /** Attribute name customers can use to set or suppress anchors. Default: 'data-amp-track-id'. */\n explicitTrackingAttribute: string;\n /** Compiled regex patterns matching ids the algorithm should treat as autogenerated. */\n autogeneratedIdPatterns: RegExp[];\n /** Compiled regex patterns matching classes the fallback should filter from sibling disambiguation. */\n unstableClassPatterns: RegExp[];\n /** Optional throttle on the ancestor walk. When undefined, the walk runs to <html>. */\n maxAncestorWalkDepth?: number;\n}\n\n/**\n * Remote-config payload shape. Each field is optional; absent fields fall back\n * to built-in defaults. Customers see the defaults in the remote-config UI and\n * may add or remove patterns as needed.\n */\nexport interface ElementSelectorRemoteConfig {\n /** Kill switch. When set, overrides the default at resolve time. */\n enabled?: boolean;\n /** Override the attribute name used for explicit tracking. */\n explicitTrackingAttribute?: string;\n /** Full replacement for the autogenerated-id pattern list. When omitted, defaults apply. */\n autogeneratedIdPatterns?: string[];\n /** Full replacement for the unstable-class pattern list. When omitted, defaults apply. */\n unstableClassPatterns?: string[];\n /** Defensive throttle on the ancestor walk. Omit for unbounded walking to <html>. */\n maxAncestorWalkDepth?: number;\n}\n\n/**\n * Public interface of the runtime engine. Implementation lands in the\n * orchestration PR; the interface is declared here so consumers can write\n * against the contract before the class exists.\n */\nexport interface SelectorEngine {\n /** Produce a CSS selector string identifying `el` using the engine's current config. */\n generate(el: Element): string;\n\n /** Read-only access to the current resolved config — for diagnostics and consumer hydration. */\n getConfig(): Readonly<ResolvedSelectorConfig>;\n\n /** Replace the current config. Notifies any onConfigChange subscribers. */\n updateConfig(next: ResolvedSelectorConfig): void;\n\n /**\n * Subscribe to config-change notifications. Returns an unsubscribe function.\n * Used by the Chrome extension to keep its locally-hydrated engine in sync\n * with the customer's SDK as remote config updates flow in.\n */\n onConfigChange(cb: (config: ResolvedSelectorConfig) => void): () => void;\n}\n"]} |
+21
| MIT License | ||
| Copyright (c) 2022 Amplitude Analytics | ||
| Permission is hereby granted, free of charge, to any person obtaining a copy | ||
| of this software and associated documentation files (the "Software"), to deal | ||
| in the Software without restriction, including without limitation the rights | ||
| to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | ||
| copies of the Software, and to permit persons to whom the Software is | ||
| furnished to do so, subject to the following conditions: | ||
| The above copyright notice and this permission notice shall be included in all | ||
| copies or substantial portions of the Software. | ||
| THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | ||
| IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | ||
| FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | ||
| AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | ||
| LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | ||
| OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | ||
| SOFTWARE. |
+162
| # @amplitude/element-selector | ||
| Shared element-selector algorithm consumed by the autocapture SDK plugin, the app.amplitude.com tagging UI, and the Chrome extension visual tagger. One implementation, three surfaces, identical output. | ||
| ## Why it exists | ||
| The legacy autocapture `cssPath` algorithm anchored on any element id it found, which broke selector stability whenever a framework generated an id like `:r5:` (React's `useId`) or `radix-A1B2C3` (Radix UI). It also leaned heavily on classes for sibling disambiguation, which broke selector stability whenever a library applied transient state classes like `swiper-slide-active` or `Mui-selected`. | ||
| This package replaces that algorithm with a strategy-chain orchestrator that filters autogenerated ids and library state classes out of the anchor selection, then disambiguates siblings positionally via `:nth-of-type`. The result: selectors that survive component re-mounts, carousel interactions, hover/focus state churn, and minor design tweaks. | ||
| See the design doc at `packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md` for the full background and rationale. | ||
| ## Install | ||
| This package lives in the `Amplitude-TypeScript` monorepo and is published to npm as `@amplitude/element-selector`. Consumers in this repo declare it as a workspace dependency: | ||
| ```json | ||
| { | ||
| "dependencies": { | ||
| "@amplitude/element-selector": "workspace:*" | ||
| } | ||
| } | ||
| ``` | ||
| The only runtime dependency is `tslib`; diagnostics accept any logger with `warn` and `debug` methods. | ||
| ## Usage | ||
| ### Standalone consumer | ||
| ```ts | ||
| import { createSelectorEngine, resolveSelectorConfig } from '@amplitude/element-selector'; | ||
| // Start with the documented defaults. Remote-config values can be applied as | ||
| // they arrive — `resolveSelectorConfig` is safe to call repeatedly. | ||
| const config = resolveSelectorConfig({ enabled: true }); | ||
| const engine = createSelectorEngine(config); | ||
| // Generate a selector for any element. The engine returns the best selector | ||
| // it can produce, falling back to a positional walk when no strategy applies. | ||
| document.addEventListener('click', (event) => { | ||
| const target = event.target as Element; | ||
| const selector = engine.generate(target); | ||
| console.log(`Clicked: ${selector}`); | ||
| }); | ||
| ``` | ||
| ### Autocapture SDK plugin | ||
| The autocapture plugin instantiates one engine per `init()` call, threads its `loggerProvider` through for diagnostic logs, and forwards remote-config updates via `updateConfig`: | ||
| ```ts | ||
| import { createSelectorEngine, resolveSelectorConfig } from '@amplitude/element-selector'; | ||
| const engine = createSelectorEngine(resolveSelectorConfig(remote, config.loggerProvider), { | ||
| logger: config.loggerProvider, | ||
| }); | ||
| config.remoteConfigClient.subscribe('elementSelector', 'all', (next) => { | ||
| engine.updateConfig(resolveSelectorConfig(next, config.loggerProvider)); | ||
| }); | ||
| ``` | ||
| ### Chrome extension visual tagger | ||
| The extension reads the page's already-live engine from `window.amplitude.elementSelector` (when present) and subscribes to `onConfigChange` so its preview stays in sync with the customer's runtime config: | ||
| ```ts | ||
| const pageEngine = (window as any).amplitude?.elementSelector; | ||
| if (pageEngine) { | ||
| const unsubscribe = pageEngine.onConfigChange((nextConfig) => { | ||
| rerenderTaggingUI(nextConfig); | ||
| }); | ||
| // Call unsubscribe() on extension teardown. | ||
| } | ||
| ``` | ||
| ## How the algorithm works | ||
| The orchestrator walks from the clicked element up through its ancestors, trying each strategy at each level in priority order. The first strategy that produces a unique-resolving selector wins; the trail of elements between the anchor and the original target is expressed as a positional descent. | ||
| ### Strategy chain | ||
| | Order | Strategy | Anchor format | When it applies | | ||
| |-------|----------|--------------|-----------------| | ||
| | 1 | `explicitTrackingAttribute` | `[data-amp-track-id="value"]` | Customer set the tracking attribute with a non-empty value | | ||
| | 2 | `stableId` | `tag#id` | Element has an id, the id isn't suppressed, and it doesn't match any autogenerated-id pattern | | ||
| ### Fallback | ||
| When neither strategy produces a unique selector anywhere on the walk, the engine invokes `fallbackCssPath`. The fallback walks from the target back up to `<html>`, expressing each step as `tag:nth-of-type(n)` and terminating early at the first unique id that survives the autogenerated-id filter. | ||
| ### Sibling disambiguation: `:nth-of-type`, never classes | ||
| The fallback does not use classes for sibling disambiguation. Classes are runtime-volatile in modern frameworks — Tailwind utilities change with design tweaks, CSS-in-JS hashes change every build, and library state classes (`swiper-slide-active`, `is-open`, `Mui-selected`) move between elements as the user interacts. Positional descent via `:nth-of-type(n)` is stable across all of these. | ||
| ## Defaults | ||
| ### `enabled: false` | ||
| v1 ships dormant. Autocapture continues to use the legacy `cssPath` until the remote-config payload flips `enabled` to `true` for an org. | ||
| ### `explicitTrackingAttribute: "data-amp-track-id"` | ||
| Customers can set this attribute on an element to anchor the selector on it explicitly. Setting it to an empty string is a suppression signal — the engine treats the element's id as if it didn't exist. | ||
| ### `autogeneratedIdPatterns` | ||
| The default list filters out ids that look generated rather than authored: | ||
| | Pattern | Catches | Example | | ||
| |---------|---------|---------| | ||
| | `^:r[0-9a-z]+:$` | React `useId` | `:r5:`, `:rk:` | | ||
| | `^radix-` | Radix UI primitives | `radix-A1B2C3` | | ||
| | `^headlessui-` | Headless UI components | `headlessui-menu-button-:r5:` | | ||
| | `^mui-` | MUI internal id prefix | `mui-12345` | | ||
| | `^[a-f0-9]{16,}$` (`i`) | UUIDs without hyphens, build hashes | `a9b8c7d6e5f4321b` | | ||
| | `-\d{8,}$` | Trailing long digit runs | `session-1700000000` | | ||
| | `\d{4,}` | Any 4+ consecutive digits | `product-2134`, `row-1234` | | ||
| ### `unstableClassPatterns` | ||
| The default list (used by the fallback) catches classes that look stable in name but aren't stable in practice — Tailwind utilities, CSS-in-JS hashes (Emotion, styled-components, CSS modules, styled-jsx), and library runtime state classes (Swiper, MUI, Radix, BEM-style `is-active`). | ||
| ### `maxAncestorWalkDepth: undefined` | ||
| By default the walk runs all the way to `<html>`. Set this to a positive integer to cap the depth — useful for catastrophically deep DOMs where uniqueness checks could become expensive. | ||
| ## Remote config | ||
| The remote-config payload shape is defined in `src/types.ts` as `ElementSelectorRemoteConfig` and mirrored as a JSON Schema at `schema/element-selector-remote-config.schema.json` for the dashboard's remote-config editor and backend validation. | ||
| Customer-provided pattern lists fully **replace** the defaults — they're not merged. An empty array (`autogeneratedIdPatterns: []`) is an explicit "disable filtering" signal, not a "use defaults" signal. Invalid regex strings are dropped (with a warn log) rather than crashing the engine. | ||
| ## Diagnostics | ||
| Every path that catches an error accepts an optional `ElementSelectorLogger`, which is structurally compatible with the SDK's `@amplitude/analytics-core` logger provider. When wired up, the engine surfaces: | ||
| - `warn` — invalid regex strings in remote config, subscriber exceptions during `updateConfig` fan-out | ||
| - `debug` — malformed selectors produced by a strategy | ||
| When no logger is provided, the engine stays silent — preserving fire-and-forget semantics for consumers that don't want diagnostic noise. | ||
| ## Testing | ||
| ```bash | ||
| pnpm --filter @amplitude/element-selector test | ||
| pnpm --filter @amplitude/element-selector lint | ||
| pnpm --filter @amplitude/element-selector build | ||
| ``` | ||
| The test suite includes: | ||
| - **Primitive unit tests** for the pattern packs, helpers, strategies, config resolver, orchestrator, fallback, and engine factory | ||
| - **Schema drift guard** — asserts the JSON Schema and the `ElementSelectorRemoteConfig` TypeScript type stay in sync | ||
| - **Scenarios corpus** — a parameterized differential test that runs the engine over hand-picked fixtures (Swiper, MUI, Tailwind, semantic HTML, CSS-in-JS soup, etc.) and asserts both the expected selector and a `querySelector` round-trip | ||
| - **Property tests** — generate random DOM trees with mixed stable / autogen ids and class soup, assert the engine always emits a selector that uniquely resolves back to the target | ||
| ## Design references | ||
| - Design doc: `packages/plugin-autocapture-browser/element-selector-strategy-v1-no-classes.md` | ||
| - Implementation plan: phased PR breakdown in the design doc's "Rollout" section |
+40
-5
| { | ||
| "name": "@amplitude/element-selector", | ||
| "version": "0.0.1", | ||
| "description": "Bootstrap placeholder for Trusted Publishing setup. Do not use.", | ||
| "private": false, | ||
| "version": "0.1.0", | ||
| "description": "Shared element-selector algorithm consumed by autocapture, the Amplitude dashboard, and the Chrome extension visual tagger.", | ||
| "author": "Amplitude Inc", | ||
| "homepage": "https://github.com/amplitude/Amplitude-TypeScript", | ||
| "license": "MIT", | ||
| "main": "lib/cjs/index.js", | ||
| "module": "lib/esm/index.js", | ||
| "types": "lib/esm/index.d.ts", | ||
| "sideEffects": false, | ||
| "publishConfig": { | ||
| "access": "public" | ||
| "access": "public", | ||
| "tag": "latest" | ||
| }, | ||
| "repository": { | ||
| "type": "git", | ||
| "url": "git+https://github.com/amplitude/Amplitude-TypeScript.git" | ||
| }, | ||
| "bugs": { | ||
| "url": "https://github.com/amplitude/Amplitude-TypeScript/issues" | ||
| }, | ||
| "dependencies": { | ||
| "tslib": "^2.4.1" | ||
| }, | ||
| "files": [ | ||
| "lib" | ||
| ], | ||
| "scripts": { | ||
| "build": "pnpm build:es5 & pnpm build:esm", | ||
| "build:es5": "tsc -p ./tsconfig.es5.json", | ||
| "build:esm": "tsc -p ./tsconfig.esm.json", | ||
| "watch": "tsc -p ./tsconfig.esm.json --watch", | ||
| "clean": "rimraf node_modules lib coverage", | ||
| "fix": "pnpm fix:eslint & pnpm fix:prettier", | ||
| "fix:eslint": "eslint '{src,test}/**/*.ts' --fix", | ||
| "fix:prettier": "prettier --write \"{src,test}/**/*.ts\"", | ||
| "lint": "pnpm lint:eslint & pnpm lint:prettier", | ||
| "lint:eslint": "eslint '{src,test}/**/*.ts'", | ||
| "lint:prettier": "prettier --check \"{src,test}/**/*.ts\"", | ||
| "test": "jest", | ||
| "typecheck": "tsc -p ./tsconfig.json" | ||
| } | ||
| } | ||
| } |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Empty package
Supply chain riskPackage does not contain any code. It may be removed, is name squatting, or the result of a faulty package publish.
No README
QualityPackage does not have a README. This may indicate a failed publish or a low quality package.
No contributors or author data
MaintenancePackage does not specify a list of contributors or an author in package.json.
No bug tracker
MaintenancePackage does not have a linked bug tracker in package.json.
No License Found
LicenseLicense information could not be found.
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
No tests
QualityPackage does not have any tests. This is a strong signal of a poorly maintained or low quality package.
No website
QualityPackage does not have a website.
291094
132819.63%115
11400%0
-100%3013
Infinity%0
-100%1
-66.67%0
-100%163
Infinity%0
-100%1
Infinity%+ Added
+ Added