@wix/zero-config-implementation 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +72 -0
  2. package/dist/component-loader.d.ts +42 -0
  3. package/dist/component-renderer.d.ts +31 -0
  4. package/dist/converters/data-item-builder.d.ts +15 -0
  5. package/dist/converters/index.d.ts +1 -0
  6. package/dist/converters/to-editor-component.d.ts +3 -0
  7. package/dist/converters/utils.d.ts +16 -0
  8. package/dist/errors.d.ts +230 -0
  9. package/dist/index.d.ts +42 -0
  10. package/dist/index.js +51978 -0
  11. package/dist/information-extractors/css/index.d.ts +3 -0
  12. package/dist/information-extractors/css/parse.d.ts +7 -0
  13. package/dist/information-extractors/css/selector-matcher.d.ts +3 -0
  14. package/dist/information-extractors/css/types.d.ts +49 -0
  15. package/dist/information-extractors/react/extractors/core/index.d.ts +6 -0
  16. package/dist/information-extractors/react/extractors/core/runner.d.ts +19 -0
  17. package/dist/information-extractors/react/extractors/core/store.d.ts +17 -0
  18. package/dist/information-extractors/react/extractors/core/tree-builder.d.ts +15 -0
  19. package/dist/information-extractors/react/extractors/core/types.d.ts +40 -0
  20. package/dist/information-extractors/react/extractors/css-properties.d.ts +20 -0
  21. package/dist/information-extractors/react/extractors/index.d.ts +11 -0
  22. package/dist/information-extractors/react/extractors/prop-tracker.d.ts +24 -0
  23. package/dist/information-extractors/react/index.d.ts +9 -0
  24. package/dist/information-extractors/react/types.d.ts +51 -0
  25. package/dist/information-extractors/react/utils/mock-generator.d.ts +9 -0
  26. package/dist/information-extractors/react/utils/prop-spy.d.ts +10 -0
  27. package/dist/information-extractors/ts/components.d.ts +9 -0
  28. package/dist/information-extractors/ts/css-imports.d.ts +2 -0
  29. package/dist/information-extractors/ts/index.d.ts +3 -0
  30. package/dist/information-extractors/ts/types.d.ts +47 -0
  31. package/dist/information-extractors/ts/utils/semantic-type-resolver.d.ts +3 -0
  32. package/dist/jsx-runtime-interceptor.d.ts +42 -0
  33. package/dist/jsx-runtime-interceptor.js +63 -0
  34. package/dist/jsx-runtime-loader.d.ts +23 -0
  35. package/dist/jsx-runtime-loader.js +7 -0
  36. package/dist/manifest-pipeline.d.ts +33 -0
  37. package/dist/schema.d.ts +167 -0
  38. package/dist/ts-compiler.d.ts +13 -0
  39. package/package.json +81 -0
  40. package/src/component-loader.test.ts +277 -0
  41. package/src/component-loader.ts +256 -0
  42. package/src/component-renderer.ts +192 -0
  43. package/src/converters/data-item-builder.ts +354 -0
  44. package/src/converters/index.ts +1 -0
  45. package/src/converters/to-editor-component.ts +167 -0
  46. package/src/converters/utils.ts +21 -0
  47. package/src/errors.ts +103 -0
  48. package/src/index.ts +223 -0
  49. package/src/information-extractors/css/README.md +3 -0
  50. package/src/information-extractors/css/index.ts +3 -0
  51. package/src/information-extractors/css/parse.ts +450 -0
  52. package/src/information-extractors/css/selector-matcher.ts +88 -0
  53. package/src/information-extractors/css/types.ts +56 -0
  54. package/src/information-extractors/react/extractors/core/index.ts +6 -0
  55. package/src/information-extractors/react/extractors/core/runner.ts +89 -0
  56. package/src/information-extractors/react/extractors/core/store.ts +36 -0
  57. package/src/information-extractors/react/extractors/core/tree-builder.ts +273 -0
  58. package/src/information-extractors/react/extractors/core/types.ts +48 -0
  59. package/src/information-extractors/react/extractors/css-properties.ts +214 -0
  60. package/src/information-extractors/react/extractors/index.ts +27 -0
  61. package/src/information-extractors/react/extractors/prop-tracker.ts +132 -0
  62. package/src/information-extractors/react/index.ts +53 -0
  63. package/src/information-extractors/react/types.ts +70 -0
  64. package/src/information-extractors/react/utils/mock-generator.ts +331 -0
  65. package/src/information-extractors/react/utils/prop-spy.ts +168 -0
  66. package/src/information-extractors/ts/components.ts +300 -0
  67. package/src/information-extractors/ts/css-imports.ts +26 -0
  68. package/src/information-extractors/ts/index.ts +3 -0
  69. package/src/information-extractors/ts/types.ts +56 -0
  70. package/src/information-extractors/ts/utils/semantic-type-resolver.ts +377 -0
  71. package/src/jsx-runtime-interceptor.ts +146 -0
  72. package/src/jsx-runtime-loader.ts +38 -0
  73. package/src/manifest-pipeline.ts +362 -0
  74. package/src/schema.ts +174 -0
  75. package/src/ts-compiler.ts +41 -0
  76. package/tsconfig.json +17 -0
  77. package/typedoc.json +18 -0
  78. package/vite.config.ts +45 -0
@@ -0,0 +1,88 @@
1
+ import * as cheerio from 'cheerio'
2
+ import { TRACE_ATTR } from '../../component-renderer'
3
+ import type { ExtractedCssInfo } from '../../index'
4
+ import type { ExtractedElement } from '../react/extractors/core/tree-builder'
5
+ import type { CSSProperty, CssSelectorMatch, MatchedCssData } from './types'
6
+
7
+ export function matchCssSelectors(
8
+ html: string,
9
+ elements: ExtractedElement[],
10
+ cssInfos: ExtractedCssInfo[],
11
+ ): ExtractedElement[] {
12
+ const $ = cheerio.load(html)
13
+
14
+ // Build traceId -> matches map
15
+ const matchesByTraceId = new Map<string, CssSelectorMatch[]>()
16
+ const customPropsByTraceId = new Map<string, Record<string, string>>()
17
+
18
+ for (const cssInfo of cssInfos) {
19
+ const allProps = cssInfo.api.getAllProperties()
20
+
21
+ for (const [selector, properties] of allProps) {
22
+ // Skip @keyframes - not element selectors
23
+ if (selector.startsWith('@')) continue
24
+
25
+ // Get DOM-matchable selector from the API
26
+ const domSelector = cssInfo.api.getDomSelector(selector)
27
+ if (!domSelector) continue
28
+
29
+ try {
30
+ $(domSelector).each((_, elem) => {
31
+ const traceId = $(elem).attr(TRACE_ATTR)
32
+ if (!traceId) return
33
+
34
+ // Use reduce to separate regular and custom properties in one pass
35
+ const { regular, custom } = properties.reduce<{
36
+ regular: CSSProperty[]
37
+ custom: Record<string, string>
38
+ }>(
39
+ (acc, prop) => {
40
+ if (prop.name.startsWith('--')) {
41
+ acc.custom[prop.name] = prop.value
42
+ } else {
43
+ acc.regular.push(prop)
44
+ }
45
+ return acc
46
+ },
47
+ { regular: [], custom: {} },
48
+ )
49
+
50
+ if (regular.length > 0) {
51
+ const existing = matchesByTraceId.get(traceId) ?? []
52
+ existing.push({ selector, properties: regular })
53
+ matchesByTraceId.set(traceId, existing)
54
+ }
55
+
56
+ if (Object.keys(custom).length > 0) {
57
+ const existing = customPropsByTraceId.get(traceId) ?? {}
58
+ Object.assign(existing, custom)
59
+ customPropsByTraceId.set(traceId, existing)
60
+ }
61
+ })
62
+ } catch {
63
+ // Invalid selector for Cheerio, skip
64
+ }
65
+ }
66
+ }
67
+
68
+ return enrichElements(elements, matchesByTraceId, customPropsByTraceId)
69
+ }
70
+
71
+ function enrichElements(
72
+ elements: ExtractedElement[],
73
+ matchesByTraceId: Map<string, CssSelectorMatch[]>,
74
+ customPropsByTraceId: Map<string, Record<string, string>>,
75
+ ): ExtractedElement[] {
76
+ return elements.map((el) => {
77
+ const data: MatchedCssData = {
78
+ matches: matchesByTraceId.get(el.traceId) ?? [],
79
+ customProperties: customPropsByTraceId.get(el.traceId) ?? {},
80
+ }
81
+ el.extractorData.set('css-matcher', data)
82
+
83
+ return {
84
+ ...el,
85
+ children: enrichElements(el.children, matchesByTraceId, customPropsByTraceId),
86
+ }
87
+ })
88
+ }
@@ -0,0 +1,56 @@
1
+ export interface CSSProperty {
2
+ name: string
3
+ value: string
4
+ }
5
+
6
+ export interface CssSelectorMatch {
7
+ selector: string
8
+ properties: CSSProperty[]
9
+ }
10
+
11
+ export interface MatchedCssData {
12
+ matches: CssSelectorMatch[]
13
+ customProperties: Record<string, string>
14
+ }
15
+
16
+ /**
17
+ * API returned by parseCss function for querying parsed CSS
18
+ */
19
+ export interface CSSParserAPI {
20
+ /**
21
+ * Gets CSS properties for a specific selector
22
+ * @param selector - The selector to extract properties for
23
+ * @returns Array of CSS properties for the specified selector
24
+ */
25
+ getPropertiesForSelector: (selector: string) => CSSProperty[]
26
+
27
+ /**
28
+ * Gets all selectors and their properties from the CSS
29
+ * @returns Map from selector to its CSS properties
30
+ */
31
+ getAllProperties: () => Map<string, CSSProperty[]>
32
+
33
+ /**
34
+ * Gets all CSS property names that use a specific CSS variable
35
+ * @param varName - The CSS variable name (with or without --)
36
+ * @returns Array of CSS property names that use this variable
37
+ */
38
+ getVarUsages: (varName: string) => string[]
39
+
40
+ /**
41
+ * Gets unique properties from multiple selectors with CSS cascade rules:
42
+ * - First selector wins (properties from earlier selectors take precedence)
43
+ * - Within a selector, last value wins (if property appears multiple times)
44
+ * @param selectors - Array of selectors to extract properties from
45
+ * @returns Map of property names to their values
46
+ */
47
+ getUniqueProperties: (selectors: string[]) => Map<string, string>
48
+
49
+ /**
50
+ * Gets a DOM-matchable selector by stripping pseudo-classes.
51
+ * Returns null if the selector contains pseudo-elements (unmatchable against real DOM).
52
+ * @param selector - The original CSS selector string
53
+ * @returns DOM-matchable selector string or null
54
+ */
55
+ getDomSelector: (selector: string) => string | null
56
+ }
@@ -0,0 +1,6 @@
1
+ export { ExtractorStore } from './store'
2
+ export { runExtractors } from './runner'
3
+ export type { ExtractionResult } from './runner'
4
+ export { buildElementTree } from './tree-builder'
5
+ export type { ExtractedElement } from './tree-builder'
6
+ export type { ReactExtractor, RenderContext, CreateElementEvent, RenderCompleteEvent } from './types'
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Generic Extractor Runner
3
+ *
4
+ * Orchestrates the extractor lifecycle:
5
+ * 1. Create shared ExtractorStore
6
+ * 2. Run beforeRender phase (extractors can modify props)
7
+ * 3. Render with onCreateElement hooks
8
+ * 4. Run renderComplete phase
9
+ * 5. Parse HTML and merge with store data
10
+ */
11
+
12
+ import type { ComponentType } from 'react'
13
+ import { type CreateElementListener, renderWithExtractors } from '../../../../component-renderer'
14
+ import type { ComponentInfo } from '../../../ts/types'
15
+ import { generateMockProps, resetMockCounter } from '../../utils/mock-generator'
16
+ import { ExtractorStore } from './store'
17
+ import { type ExtractedElement, buildElementTree } from './tree-builder'
18
+ import type { CreateElementEvent, ReactExtractor, RenderContext } from './types'
19
+
20
+ // ─────────────────────────────────────────────────────────────────────────────
21
+ // Types
22
+ // ─────────────────────────────────────────────────────────────────────────────
23
+
24
+ export interface ExtractionResult {
25
+ html: string
26
+ store: ExtractorStore
27
+ elements: ExtractedElement[]
28
+ }
29
+
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+ // Runner
32
+ // ─────────────────────────────────────────────────────────────────────────────
33
+
34
+ /**
35
+ * Runs extractors through the full lifecycle and returns extraction results.
36
+ *
37
+ * @param componentInfo - TypeScript-extracted component information
38
+ * @param component - The React component to render
39
+ * @param extractors - Array of extractors to run
40
+ * @returns Extraction results including HTML, store, and element tree
41
+ */
42
+ export function runExtractors(
43
+ componentInfo: ComponentInfo,
44
+ component: ComponentType<unknown>,
45
+ extractors: ReactExtractor[],
46
+ ): ExtractionResult {
47
+ // Reset mock counter for reproducible results
48
+ resetMockCounter()
49
+
50
+ // Create shared store
51
+ const store = new ExtractorStore()
52
+
53
+ // Create render context with mutable props
54
+ const context: RenderContext = {
55
+ componentInfo,
56
+ component,
57
+ props: generateMockProps(componentInfo),
58
+ store,
59
+ }
60
+
61
+ // Phase 1: beforeRender - extractors can modify props
62
+ for (const ext of extractors) {
63
+ ext.onBeforeRender?.(context)
64
+ }
65
+
66
+ // Create listeners that call onCreateElement for each extractor
67
+ const listeners: CreateElementListener[] = [
68
+ {
69
+ onCreateElement(event: CreateElementEvent): void {
70
+ for (const ext of extractors) {
71
+ ext.onCreateElement?.(event)
72
+ }
73
+ },
74
+ },
75
+ ]
76
+
77
+ // Phase 2: Render with element creation interception
78
+ const html = renderWithExtractors(context.component, context.props, listeners, store)
79
+
80
+ // Phase 3: renderComplete - extractors can post-process
81
+ for (const ext of extractors) {
82
+ ext.onRenderComplete?.({ html, context })
83
+ }
84
+
85
+ // Phase 4: Parse HTML and merge with store data
86
+ const elements = buildElementTree(html, store)
87
+
88
+ return { html, store, elements }
89
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * ExtractorStore - Shared store for extractors with namespace support
3
+ *
4
+ * Extractors write to this store during render, namespaced by their name:
5
+ * store.set(traceId, 'prop-tracker', { boundProps: ['label', 'onClick'] })
6
+ * store.set(traceId, 'css-properties', { relevant: ['color', 'background'] })
7
+ *
8
+ * Store structure: Map<traceId, Map<extractorName, data>>
9
+ */
10
+
11
+ export class ExtractorStore {
12
+ private data = new Map<string, Map<string, unknown>>()
13
+
14
+ set<T>(traceId: string, extractorName: string, value: T): void {
15
+ if (!this.data.has(traceId)) {
16
+ this.data.set(traceId, new Map())
17
+ }
18
+ this.data.get(traceId)!.set(extractorName, value)
19
+ }
20
+
21
+ get<T>(traceId: string, extractorName: string): T | undefined {
22
+ return this.data.get(traceId)?.get(extractorName) as T | undefined
23
+ }
24
+
25
+ getAll(traceId: string): Map<string, unknown> | undefined {
26
+ return this.data.get(traceId)
27
+ }
28
+
29
+ entries(): IterableIterator<[string, Map<string, unknown>]> {
30
+ return this.data.entries()
31
+ }
32
+
33
+ clear(): void {
34
+ this.data.clear()
35
+ }
36
+ }
@@ -0,0 +1,273 @@
1
+ /**
2
+ * Build Element Tree
3
+ *
4
+ * Parses HTML to get hierarchy and merges with ExtractorStore data.
5
+ * Each element gets a semantic name computed by concatenating ancestor names.
6
+ */
7
+
8
+ import { pascalCase } from 'case-anything'
9
+ import { type DefaultTreeAdapterMap, parseFragment } from 'parse5'
10
+ import { TRACE_ATTR } from '../../../../component-renderer'
11
+ import type { CssPropertiesData } from '../css-properties'
12
+ import { addTextProperties } from '../css-properties'
13
+ import type { ExtractorStore } from './store'
14
+
15
+ // ─────────────────────────────────────────────────────────────────────────────
16
+ // Types
17
+ // ─────────────────────────────────────────────────────────────────────────────
18
+
19
+ export interface ExtractedElement {
20
+ traceId: string
21
+ name: string // Semantic name for the element (see naming rules)
22
+ tag: string
23
+ attributes: Record<string, string>
24
+ extractorData: Map<string, unknown> // extractorName → data
25
+ children: ExtractedElement[]
26
+ hasTextContent?: boolean
27
+ }
28
+
29
+ // ─────────────────────────────────────────────────────────────────────────────
30
+ // Tag Normalization Map
31
+ // ─────────────────────────────────────────────────────────────────────────────
32
+
33
+ const TAG_NAMES: Record<string, string> = {
34
+ a: 'Anchor',
35
+ abbr: 'Abbreviation',
36
+ article: 'Article',
37
+ aside: 'Aside',
38
+ button: 'Button',
39
+ caption: 'Caption',
40
+ col: 'TableColumn',
41
+ colgroup: 'TableColumnGroup',
42
+ dd: 'DescriptionDetails',
43
+ details: 'Details',
44
+ div: 'Div',
45
+ dl: 'DescriptionList',
46
+ dt: 'DescriptionTerm',
47
+ fieldset: 'Fieldset',
48
+ figcaption: 'FigureCaption',
49
+ figure: 'Figure',
50
+ footer: 'Footer',
51
+ form: 'Form',
52
+ h1: 'Heading1',
53
+ h2: 'Heading2',
54
+ h3: 'Heading3',
55
+ h4: 'Heading4',
56
+ h5: 'Heading5',
57
+ h6: 'Heading6',
58
+ header: 'Header',
59
+ hr: 'HorizontalRule',
60
+ img: 'Image',
61
+ input: 'Input',
62
+ label: 'Label',
63
+ legend: 'Legend',
64
+ li: 'ListItem',
65
+ main: 'Main',
66
+ nav: 'Navigation',
67
+ ol: 'OrderedList',
68
+ optgroup: 'OptionGroup',
69
+ option: 'Option',
70
+ p: 'Paragraph',
71
+ pre: 'Preformatted',
72
+ progress: 'Progress',
73
+ section: 'Section',
74
+ select: 'Select',
75
+ span: 'Span',
76
+ strong: 'Strong',
77
+ summary: 'Summary',
78
+ table: 'Table',
79
+ tbody: 'TableBody',
80
+ td: 'TableCell',
81
+ textarea: 'TextArea',
82
+ tfoot: 'TableFooter',
83
+ th: 'TableHeader',
84
+ thead: 'TableHead',
85
+ tr: 'TableRow',
86
+ ul: 'UnorderedList',
87
+ }
88
+
89
+ // ─────────────────────────────────────────────────────────────────────────────
90
+ // Helpers
91
+ // ─────────────────────────────────────────────────────────────────────────────
92
+
93
+ type Element = DefaultTreeAdapterMap['element']
94
+ type TextNode = DefaultTreeAdapterMap['textNode']
95
+ type Node = DefaultTreeAdapterMap['node']
96
+
97
+ const isElement = (node: Node): node is Element => 'tagName' in node
98
+ const isTextNode = (node: Node): node is TextNode => 'nodeName' in node && node.nodeName === '#text'
99
+
100
+ const getAttribute = (element: Element, name: string): string | undefined =>
101
+ element.attrs.find((attr) => attr.name === name)?.value
102
+
103
+ const getAttributes = (element: Element): Record<string, string> => {
104
+ const attrs: Record<string, string> = {}
105
+ for (const attr of element.attrs) {
106
+ attrs[attr.name] = attr.value
107
+ }
108
+ return attrs
109
+ }
110
+
111
+ /**
112
+ * Checks if an element has direct text content (non-whitespace text nodes as children)
113
+ */
114
+ const hasDirectTextContent = (element: Element): boolean =>
115
+ element.childNodes.some((child) => isTextNode(child) && child.value.trim().length > 0)
116
+
117
+ /**
118
+ * Normalizes a tag name to its semantic PascalCase form
119
+ */
120
+ function normalizeTagName(tag: string): string {
121
+ return TAG_NAMES[tag.toLowerCase()] ?? pascalCase(tag)
122
+ }
123
+
124
+ /**
125
+ * Gets the text content of an element (for aria-labelledby resolution)
126
+ */
127
+ function getTextContent(element: Element): string {
128
+ let text = ''
129
+ for (const child of element.childNodes) {
130
+ if (isTextNode(child)) {
131
+ text += child.value
132
+ } else if (isElement(child)) {
133
+ text += getTextContent(child)
134
+ }
135
+ }
136
+ return text.trim()
137
+ }
138
+
139
+ // ─────────────────────────────────────────────────────────────────────────────
140
+ // Element Naming
141
+ // ─────────────────────────────────────────────────────────────────────────────
142
+
143
+ /**
144
+ * Computes the name contribution for a single element.
145
+ * Priority: id > aria-label > aria-labelledby > normalized tag name
146
+ */
147
+ function getElementNamePart(element: Element, getElementById: (id: string) => Element | undefined): string {
148
+ const id = getAttribute(element, 'id')
149
+ if (id) {
150
+ return pascalCase(id)
151
+ }
152
+
153
+ const ariaLabel = getAttribute(element, 'aria-label')
154
+ if (ariaLabel) {
155
+ return pascalCase(ariaLabel)
156
+ }
157
+
158
+ const ariaLabelledBy = getAttribute(element, 'aria-labelledby')
159
+ if (ariaLabelledBy) {
160
+ const labelElement = getElementById(ariaLabelledBy)
161
+ if (labelElement) {
162
+ const labelText = getTextContent(labelElement)
163
+ if (labelText) {
164
+ return pascalCase(labelText)
165
+ }
166
+ }
167
+ }
168
+
169
+ return normalizeTagName(element.tagName)
170
+ }
171
+
172
+ // ─────────────────────────────────────────────────────────────────────────────
173
+ // Tree Building
174
+ // ─────────────────────────────────────────────────────────────────────────────
175
+
176
+ /**
177
+ * Builds an element tree from HTML, merging with store data.
178
+ * Each element gets a semantic name from concatenated ancestor names.
179
+ */
180
+ export function buildElementTree(html: string, store: ExtractorStore): ExtractedElement[] {
181
+ const fragment = parseFragment(html)
182
+
183
+ // Build ID lookup for aria-labelledby resolution
184
+ const elementsById = new Map<string, Element>()
185
+ const collectIds = (node: Node): void => {
186
+ if (isElement(node)) {
187
+ const id = getAttribute(node, 'id')
188
+ if (id) {
189
+ elementsById.set(id, node)
190
+ }
191
+ node.childNodes.forEach(collectIds)
192
+ }
193
+ }
194
+ fragment.childNodes.forEach(collectIds)
195
+
196
+ const getElementById = (id: string): Element | undefined => elementsById.get(id)
197
+
198
+ /**
199
+ * Recursively walks the tree, building ExtractedElements.
200
+ * @param node - The current parse5 node
201
+ * @param ancestorPath - The concatenated name path from ancestors (empty for root's children)
202
+ * @param isRoot - Whether this is the root element (first traced element)
203
+ */
204
+ const walkTree = (node: Node, ancestorPath: string, isRoot: boolean): ExtractedElement[] => {
205
+ if (!isElement(node)) return []
206
+
207
+ const traceId = getAttribute(node, TRACE_ATTR)
208
+
209
+ // If this element has a traceId, it's a traced element
210
+ if (traceId) {
211
+ // Compute this element's name part
212
+ const namePart = isRoot ? 'root' : getElementNamePart(node, getElementById)
213
+
214
+ // Full name is ancestor path + this element's name (no separator)
215
+ const name = isRoot ? 'root' : ancestorPath + namePart
216
+
217
+ // Get extractor data from store
218
+ const extractorData = store.getAll(traceId) ?? new Map()
219
+
220
+ // Check for text content
221
+ const hasText = hasDirectTextContent(node)
222
+
223
+ // If element has text content and has css-properties data, add text properties
224
+ if (hasText) {
225
+ const cssData = extractorData.get('css-properties') as CssPropertiesData | undefined
226
+ if (cssData) {
227
+ const enhanced: CssPropertiesData = {
228
+ relevant: addTextProperties(cssData.relevant),
229
+ }
230
+ extractorData.set('css-properties', enhanced)
231
+ }
232
+ }
233
+
234
+ // Recursively process children
235
+ // Children use this element's full name as their ancestor path
236
+ const childPath = isRoot ? '' : name
237
+ const children = node.childNodes.flatMap((child) => walkTree(child, childPath, false))
238
+
239
+ // Build attributes (excluding trace-id)
240
+ const attributes = getAttributes(node)
241
+ delete attributes[TRACE_ATTR]
242
+
243
+ return [
244
+ {
245
+ traceId,
246
+ name,
247
+ tag: node.tagName,
248
+ attributes,
249
+ extractorData,
250
+ children,
251
+ ...(hasText && { hasTextContent: true }),
252
+ },
253
+ ]
254
+ }
255
+
256
+ // No traceId - just pass through to children with same ancestor path
257
+ return node.childNodes.flatMap((child) => walkTree(child, ancestorPath, false))
258
+ }
259
+
260
+ // Start with empty ancestor path, first traced element is root
261
+ let isFirst = true
262
+ const result: ExtractedElement[] = []
263
+
264
+ for (const node of fragment.childNodes) {
265
+ const elements = walkTree(node, '', isFirst)
266
+ if (elements.length > 0) {
267
+ result.push(...elements)
268
+ isFirst = false // Only the first traced element is root
269
+ }
270
+ }
271
+
272
+ return result
273
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Core interfaces for the pluggable extractor system
3
+ */
4
+
5
+ import type { ComponentType } from 'react'
6
+ import type { ComponentInfo } from '../../../ts/types'
7
+ import type { ExtractorStore } from './store'
8
+
9
+ /**
10
+ * Context available to extractors during the beforeRender phase.
11
+ * Props are mutable - extractors can modify them before render.
12
+ */
13
+ export interface RenderContext {
14
+ componentInfo: ComponentInfo
15
+ component: ComponentType<unknown>
16
+ props: Record<string, unknown> // Mutable - extractors can modify
17
+ store: ExtractorStore
18
+ }
19
+
20
+ /**
21
+ * Event emitted for each DOM element created during render.
22
+ */
23
+ export interface CreateElementEvent {
24
+ tag: string
25
+ props: Record<string, unknown>
26
+ traceId: string
27
+ children: unknown[]
28
+ store: ExtractorStore
29
+ }
30
+
31
+ /**
32
+ * Event emitted after render completes.
33
+ */
34
+ export interface RenderCompleteEvent {
35
+ html: string
36
+ context: RenderContext
37
+ }
38
+
39
+ /**
40
+ * Interface for pluggable extractors.
41
+ * Extractors can hook into any combination of lifecycle events.
42
+ */
43
+ export interface ReactExtractor {
44
+ name: string
45
+ onBeforeRender?(context: RenderContext): void
46
+ onCreateElement?(event: CreateElementEvent): void
47
+ onRenderComplete?(event: RenderCompleteEvent): void
48
+ }