@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.
- package/README.md +72 -0
- package/dist/component-loader.d.ts +42 -0
- package/dist/component-renderer.d.ts +31 -0
- package/dist/converters/data-item-builder.d.ts +15 -0
- package/dist/converters/index.d.ts +1 -0
- package/dist/converters/to-editor-component.d.ts +3 -0
- package/dist/converters/utils.d.ts +16 -0
- package/dist/errors.d.ts +230 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +51978 -0
- package/dist/information-extractors/css/index.d.ts +3 -0
- package/dist/information-extractors/css/parse.d.ts +7 -0
- package/dist/information-extractors/css/selector-matcher.d.ts +3 -0
- package/dist/information-extractors/css/types.d.ts +49 -0
- package/dist/information-extractors/react/extractors/core/index.d.ts +6 -0
- package/dist/information-extractors/react/extractors/core/runner.d.ts +19 -0
- package/dist/information-extractors/react/extractors/core/store.d.ts +17 -0
- package/dist/information-extractors/react/extractors/core/tree-builder.d.ts +15 -0
- package/dist/information-extractors/react/extractors/core/types.d.ts +40 -0
- package/dist/information-extractors/react/extractors/css-properties.d.ts +20 -0
- package/dist/information-extractors/react/extractors/index.d.ts +11 -0
- package/dist/information-extractors/react/extractors/prop-tracker.d.ts +24 -0
- package/dist/information-extractors/react/index.d.ts +9 -0
- package/dist/information-extractors/react/types.d.ts +51 -0
- package/dist/information-extractors/react/utils/mock-generator.d.ts +9 -0
- package/dist/information-extractors/react/utils/prop-spy.d.ts +10 -0
- package/dist/information-extractors/ts/components.d.ts +9 -0
- package/dist/information-extractors/ts/css-imports.d.ts +2 -0
- package/dist/information-extractors/ts/index.d.ts +3 -0
- package/dist/information-extractors/ts/types.d.ts +47 -0
- package/dist/information-extractors/ts/utils/semantic-type-resolver.d.ts +3 -0
- package/dist/jsx-runtime-interceptor.d.ts +42 -0
- package/dist/jsx-runtime-interceptor.js +63 -0
- package/dist/jsx-runtime-loader.d.ts +23 -0
- package/dist/jsx-runtime-loader.js +7 -0
- package/dist/manifest-pipeline.d.ts +33 -0
- package/dist/schema.d.ts +167 -0
- package/dist/ts-compiler.d.ts +13 -0
- package/package.json +81 -0
- package/src/component-loader.test.ts +277 -0
- package/src/component-loader.ts +256 -0
- package/src/component-renderer.ts +192 -0
- package/src/converters/data-item-builder.ts +354 -0
- package/src/converters/index.ts +1 -0
- package/src/converters/to-editor-component.ts +167 -0
- package/src/converters/utils.ts +21 -0
- package/src/errors.ts +103 -0
- package/src/index.ts +223 -0
- package/src/information-extractors/css/README.md +3 -0
- package/src/information-extractors/css/index.ts +3 -0
- package/src/information-extractors/css/parse.ts +450 -0
- package/src/information-extractors/css/selector-matcher.ts +88 -0
- package/src/information-extractors/css/types.ts +56 -0
- package/src/information-extractors/react/extractors/core/index.ts +6 -0
- package/src/information-extractors/react/extractors/core/runner.ts +89 -0
- package/src/information-extractors/react/extractors/core/store.ts +36 -0
- package/src/information-extractors/react/extractors/core/tree-builder.ts +273 -0
- package/src/information-extractors/react/extractors/core/types.ts +48 -0
- package/src/information-extractors/react/extractors/css-properties.ts +214 -0
- package/src/information-extractors/react/extractors/index.ts +27 -0
- package/src/information-extractors/react/extractors/prop-tracker.ts +132 -0
- package/src/information-extractors/react/index.ts +53 -0
- package/src/information-extractors/react/types.ts +70 -0
- package/src/information-extractors/react/utils/mock-generator.ts +331 -0
- package/src/information-extractors/react/utils/prop-spy.ts +168 -0
- package/src/information-extractors/ts/components.ts +300 -0
- package/src/information-extractors/ts/css-imports.ts +26 -0
- package/src/information-extractors/ts/index.ts +3 -0
- package/src/information-extractors/ts/types.ts +56 -0
- package/src/information-extractors/ts/utils/semantic-type-resolver.ts +377 -0
- package/src/jsx-runtime-interceptor.ts +146 -0
- package/src/jsx-runtime-loader.ts +38 -0
- package/src/manifest-pipeline.ts +362 -0
- package/src/schema.ts +174 -0
- package/src/ts-compiler.ts +41 -0
- package/tsconfig.json +17 -0
- package/typedoc.json +18 -0
- package/vite.config.ts +45 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { createRequire } from 'node:module'
|
|
2
|
+
import { dirname, join, resolve } from 'node:path'
|
|
3
|
+
import { pascalCase } from 'case-anything'
|
|
4
|
+
import { ResultAsync, errAsync, okAsync } from 'neverthrow'
|
|
5
|
+
import { getPackageEntryPoints } from 'pkg-entry-points'
|
|
6
|
+
import type { ComponentType } from 'react'
|
|
7
|
+
import { readPackageUpSync } from 'read-package-up'
|
|
8
|
+
import { IoError, NotFoundError } from './errors'
|
|
9
|
+
|
|
10
|
+
type NotFoundErrorInstance = InstanceType<typeof NotFoundError>
|
|
11
|
+
type IoErrorInstance = InstanceType<typeof IoError>
|
|
12
|
+
export type LoaderError = NotFoundErrorInstance | IoErrorInstance
|
|
13
|
+
|
|
14
|
+
export interface ComponentLoaderResult {
|
|
15
|
+
loadComponent: (name: string) => ComponentType<unknown> | null
|
|
16
|
+
packageName: string
|
|
17
|
+
entryPath: string | undefined
|
|
18
|
+
exportNames: string[]
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Creates a component loader that dynamically loads components from the
|
|
23
|
+
* user's package. Finds the nearest package.json, resolves its entry point,
|
|
24
|
+
* and loads the module.
|
|
25
|
+
*
|
|
26
|
+
* The returned `loadComponent` is synchronous (required by processComponent).
|
|
27
|
+
* ESM modules are loaded via async `import()` during creation, so this
|
|
28
|
+
* function itself is async.
|
|
29
|
+
*
|
|
30
|
+
* @returns A `ResultAsync` containing a `ComponentLoaderResult` on success.
|
|
31
|
+
* @errors {NotFoundError} When no package.json is found or the package has no resolvable entry points.
|
|
32
|
+
* @errors {IoError} When module loading fails (CJS require or ESM import).
|
|
33
|
+
*/
|
|
34
|
+
export function createComponentLoader(componentPath: string): ResultAsync<ComponentLoaderResult, LoaderError> {
|
|
35
|
+
// 1. Find nearest package.json
|
|
36
|
+
const result = readPackageUpSync({ cwd: dirname(resolve(componentPath)) })
|
|
37
|
+
if (!result) {
|
|
38
|
+
return errAsync(new NotFoundError('No package.json found', { props: { phase: 'load' } }))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { packageJson, path: packageJsonPath } = result
|
|
42
|
+
const packageDir = dirname(packageJsonPath)
|
|
43
|
+
const packageName = packageJson.name
|
|
44
|
+
|
|
45
|
+
// 2. Resolve entry point(s) via pkg-entry-points (handles exports + legacy main/module)
|
|
46
|
+
return ResultAsync.fromPromise(
|
|
47
|
+
getPackageEntryPoints(packageDir),
|
|
48
|
+
(err) =>
|
|
49
|
+
new IoError(`Failed to resolve entry points for "${packageName}"`, {
|
|
50
|
+
cause: err instanceof Error ? err : new Error(String(err)),
|
|
51
|
+
props: { phase: 'load' },
|
|
52
|
+
}),
|
|
53
|
+
).andThen((entryPoints) => {
|
|
54
|
+
return loadFromEntryPoints(entryPoints, packageDir, packageName, packageJsonPath)
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Given resolved entry points, load the appropriate module(s) and construct the
|
|
60
|
+
* `ComponentLoaderResult`.
|
|
61
|
+
*
|
|
62
|
+
* @returns A `ResultAsync` containing a `ComponentLoaderResult` on success.
|
|
63
|
+
* @errors {NotFoundError} When the package has no resolvable or loadable entry points.
|
|
64
|
+
* @errors {IoError} When module loading fails.
|
|
65
|
+
*/
|
|
66
|
+
function loadFromEntryPoints(
|
|
67
|
+
entryPoints: Record<string, [string[], string][]>,
|
|
68
|
+
packageDir: string,
|
|
69
|
+
packageName: string | undefined,
|
|
70
|
+
packageJsonPath: string,
|
|
71
|
+
): ResultAsync<ComponentLoaderResult, LoaderError> {
|
|
72
|
+
const subpaths = Object.keys(entryPoints)
|
|
73
|
+
|
|
74
|
+
if (subpaths.length === 0) {
|
|
75
|
+
return errAsync(
|
|
76
|
+
new NotFoundError(
|
|
77
|
+
`Package "${packageName}" has no resolvable entry point. Add an "exports", "module", or "main" field to ${packageJsonPath}`,
|
|
78
|
+
{ props: { phase: 'load' } },
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 3. Load module(s)
|
|
84
|
+
const dotEntry = entryPoints['.']
|
|
85
|
+
if (dotEntry || subpaths.length === 1) {
|
|
86
|
+
// Single entry point: either explicit "." or the only available subpath
|
|
87
|
+
const tuples = dotEntry ?? entryPoints[subpaths[0]]
|
|
88
|
+
const resolved = selectBestPath(tuples)
|
|
89
|
+
if (!resolved) {
|
|
90
|
+
return errAsync(
|
|
91
|
+
new NotFoundError(`Package "${packageName}" has no loadable exports`, {
|
|
92
|
+
props: { phase: 'load' },
|
|
93
|
+
}),
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
const entryPath = join(packageDir, resolved)
|
|
97
|
+
return loadModule(entryPath).map((exports) => {
|
|
98
|
+
return {
|
|
99
|
+
loadComponent: (name: string) => findComponent(exports, name),
|
|
100
|
+
packageName: packageName ?? packageDir,
|
|
101
|
+
entryPath,
|
|
102
|
+
exportNames: Object.keys(exports),
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Multiple subpaths (no ".") — load all
|
|
108
|
+
return loadAllSubpaths(entryPoints, subpaths, packageDir, packageName)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Loads all subpath entry points and merges them into a single loader result.
|
|
113
|
+
*
|
|
114
|
+
* @returns A `ResultAsync` containing a `ComponentLoaderResult` on success.
|
|
115
|
+
* @errors {NotFoundError} When none of the subpath modules could be loaded.
|
|
116
|
+
* @errors {IoError} When module loading fails.
|
|
117
|
+
*/
|
|
118
|
+
function loadAllSubpaths(
|
|
119
|
+
entryPoints: Record<string, [string[], string][]>,
|
|
120
|
+
subpaths: string[],
|
|
121
|
+
packageDir: string,
|
|
122
|
+
packageName: string | undefined,
|
|
123
|
+
): ResultAsync<ComponentLoaderResult, LoaderError> {
|
|
124
|
+
const allModules: Record<string, unknown>[] = []
|
|
125
|
+
const allExportNames: string[] = []
|
|
126
|
+
const loadErrors: IoErrorInstance[] = []
|
|
127
|
+
|
|
128
|
+
// Build a sequential chain that loads each subpath
|
|
129
|
+
let chain: ResultAsync<void, never> = okAsync(undefined)
|
|
130
|
+
|
|
131
|
+
for (const subpath of subpaths) {
|
|
132
|
+
const resolved = selectBestPath(entryPoints[subpath])
|
|
133
|
+
if (!resolved) {
|
|
134
|
+
continue
|
|
135
|
+
}
|
|
136
|
+
const subEntryPath = join(packageDir, resolved)
|
|
137
|
+
|
|
138
|
+
chain = chain.andThen(() =>
|
|
139
|
+
loadModule(subEntryPath)
|
|
140
|
+
.map((exports) => {
|
|
141
|
+
allModules.push(exports)
|
|
142
|
+
allExportNames.push(...Object.keys(exports))
|
|
143
|
+
})
|
|
144
|
+
.orElse((err) => {
|
|
145
|
+
loadErrors.push(err)
|
|
146
|
+
return okAsync(undefined)
|
|
147
|
+
}),
|
|
148
|
+
)
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return chain.andThen(() => {
|
|
152
|
+
if (allModules.length === 0) {
|
|
153
|
+
const message = loadErrors[0]?.message ?? `Package "${packageName}" has no loadable exports`
|
|
154
|
+
return errAsync(
|
|
155
|
+
new NotFoundError(message, {
|
|
156
|
+
cause: loadErrors[0],
|
|
157
|
+
props: { phase: 'load' },
|
|
158
|
+
}),
|
|
159
|
+
)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
return okAsync({
|
|
163
|
+
loadComponent: (name: string) => {
|
|
164
|
+
for (const mod of allModules) {
|
|
165
|
+
const found = findComponent(mod, name)
|
|
166
|
+
if (found) return found
|
|
167
|
+
}
|
|
168
|
+
return null
|
|
169
|
+
},
|
|
170
|
+
packageName: packageName ?? packageDir,
|
|
171
|
+
entryPath: undefined,
|
|
172
|
+
exportNames: allExportNames,
|
|
173
|
+
} satisfies ComponentLoaderResult)
|
|
174
|
+
})
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Attempts to load a module, first via ESM `import()`, then via CJS `require`.
|
|
179
|
+
*
|
|
180
|
+
* ESM `import()` is preferred because it participates in the ESM loader hook
|
|
181
|
+
* pipeline (registered via `module.register()` in the CLI). This is required
|
|
182
|
+
* for JSX interception to work — the loader hook redirects `react/jsx-runtime`
|
|
183
|
+
* to the interceptable version. CJS `require()` bypasses ESM hooks even when
|
|
184
|
+
* loading ESM modules (Node 22+), so it's only used as a fallback.
|
|
185
|
+
*
|
|
186
|
+
* @param entryPath - Absolute path to the module entry point.
|
|
187
|
+
* @returns A `ResultAsync` containing the module exports on success.
|
|
188
|
+
* @errors {IoError} When both ESM import and CJS require fail.
|
|
189
|
+
*/
|
|
190
|
+
export function loadModule(entryPath: string): ResultAsync<Record<string, unknown>, IoErrorInstance> {
|
|
191
|
+
return ResultAsync.fromPromise(import(entryPath) as Promise<Record<string, unknown>>, () => null).orElse(() => {
|
|
192
|
+
try {
|
|
193
|
+
const require = createRequire(import.meta.url)
|
|
194
|
+
const exports = require(entryPath)
|
|
195
|
+
return okAsync(exports as Record<string, unknown>)
|
|
196
|
+
} catch (requireErr) {
|
|
197
|
+
const cause = requireErr instanceof Error ? requireErr : new Error(String(requireErr))
|
|
198
|
+
return errAsync(
|
|
199
|
+
new IoError(`Failed to load ${entryPath}: ${cause.message}`, {
|
|
200
|
+
cause,
|
|
201
|
+
props: { phase: 'load' },
|
|
202
|
+
}),
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
})
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function selectBestPath(tuples: [string[], string][]): string | undefined {
|
|
209
|
+
const preferred = ['import', 'node', 'default']
|
|
210
|
+
for (const cond of preferred) {
|
|
211
|
+
const match = tuples.find(([c]) => c.includes(cond) && !c.includes('types'))
|
|
212
|
+
if (match) {
|
|
213
|
+
return match[1]
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
const fallback = tuples.find(([c]) => !c.includes('types'))?.[1]
|
|
217
|
+
return fallback
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isPascalCase(name: string): boolean {
|
|
221
|
+
return name.length > 0 && pascalCase(name) === name
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function isComponent(value: unknown): value is ComponentType<unknown> {
|
|
225
|
+
if (typeof value === 'function') return isPascalCase(value.name)
|
|
226
|
+
// React.memo() and React.forwardRef() return objects, not functions
|
|
227
|
+
if (typeof value === 'object' && value !== null && '$$typeof' in value) return true
|
|
228
|
+
return false
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function findComponent(moduleExports: Record<string, unknown>, name: string): ComponentType<unknown> | null {
|
|
232
|
+
// Direct named export
|
|
233
|
+
const direct = moduleExports[name]
|
|
234
|
+
if ((typeof direct === 'function' && isPascalCase(name)) || isComponent(direct)) {
|
|
235
|
+
return direct as ComponentType<unknown>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Check default export
|
|
239
|
+
const defaultExport = moduleExports.default
|
|
240
|
+
if (defaultExport) {
|
|
241
|
+
// default is the component itself (matches by function name)
|
|
242
|
+
if (typeof defaultExport === 'function' && defaultExport.name === name) {
|
|
243
|
+
return defaultExport as ComponentType<unknown>
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// CJS interop: default is an object with named exports
|
|
247
|
+
if (typeof defaultExport === 'object' && defaultExport !== null) {
|
|
248
|
+
const nested = (defaultExport as Record<string, unknown>)[name]
|
|
249
|
+
if (isComponent(nested)) {
|
|
250
|
+
return nested as ComponentType<unknown>
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return null
|
|
256
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Component Renderer - Renders React components with createElement interception
|
|
3
|
+
*
|
|
4
|
+
* This module intercepts React's element creation functions to track
|
|
5
|
+
* DOM element creation during server-side rendering. This allows tracking which props
|
|
6
|
+
* flow into which DOM elements.
|
|
7
|
+
*
|
|
8
|
+
* ## Interception Strategy
|
|
9
|
+
*
|
|
10
|
+
* Three complementary layers ensure interception regardless of how JSX is compiled:
|
|
11
|
+
*
|
|
12
|
+
* 1. **Vite alias** — `react/jsx-runtime` is aliased to `jsx-runtime-interceptor.ts`
|
|
13
|
+
* in vite.config.ts. This handles ESM imports resolved at build time.
|
|
14
|
+
*
|
|
15
|
+
* 2. **`React.createElement` monkey-patch** — Classic JSX transform and explicit
|
|
16
|
+
* `React.createElement()` calls are intercepted by temporarily replacing the
|
|
17
|
+
* function on the React module object.
|
|
18
|
+
*
|
|
19
|
+
* 3. **CJS module export patch** — Pre-built CJS bundles call
|
|
20
|
+
* `require('react/jsx-runtime')` at runtime, bypassing the Vite alias. We use
|
|
21
|
+
* `createRequire` to obtain the real CJS module objects and temporarily replace
|
|
22
|
+
* their `jsx`/`jsxs`/`jsxDEV` exports during rendering. This is safe because
|
|
23
|
+
* CJS module exports are mutable (`writable: true`).
|
|
24
|
+
*
|
|
25
|
+
* ## Intercepted Functions
|
|
26
|
+
*
|
|
27
|
+
* - `React.createElement` - Classic JSX transform (monkey-patched)
|
|
28
|
+
* - `jsx` / `jsxs` from `react/jsx-runtime` - New JSX transform via interceptor module
|
|
29
|
+
* - `jsx` / `jsxs` / `jsxDEV` on the real CJS `react/jsx-runtime` and
|
|
30
|
+
* `react/jsx-dev-runtime` module objects (monkey-patched for CJS consumers)
|
|
31
|
+
*
|
|
32
|
+
* ## Known Limitations
|
|
33
|
+
*
|
|
34
|
+
* 1. **Global state** - During render, we temporarily replace element creation
|
|
35
|
+
* functions globally. This is not thread-safe and could cause issues if multiple
|
|
36
|
+
* renders happen concurrently. Interceptors are restored in a finally block.
|
|
37
|
+
*
|
|
38
|
+
* 2. **React version compatibility** - This approach may break with future React versions
|
|
39
|
+
* if the internal rendering pipeline changes. Tested with React 18.x.
|
|
40
|
+
*
|
|
41
|
+
* @module component-renderer
|
|
42
|
+
*/
|
|
43
|
+
|
|
44
|
+
import { createRequire } from 'node:module'
|
|
45
|
+
import React from 'react'
|
|
46
|
+
import type { ComponentType, ReactElement } from 'react'
|
|
47
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
48
|
+
|
|
49
|
+
import type { ExtractorStore } from './information-extractors/react/extractors/core/store'
|
|
50
|
+
import type { CreateElementEvent } from './information-extractors/react/extractors/core/types'
|
|
51
|
+
|
|
52
|
+
// Import the interceptor API - use direct path since vite alias doesn't apply to all packages
|
|
53
|
+
import { getOriginals, setJsxInterceptors } from './jsx-runtime-interceptor'
|
|
54
|
+
|
|
55
|
+
export const TRACE_ATTR = 'data-trace-id'
|
|
56
|
+
|
|
57
|
+
export type { CreateElementEvent }
|
|
58
|
+
|
|
59
|
+
export interface CreateElementListener {
|
|
60
|
+
onCreateElement: (event: CreateElementEvent) => void
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
64
|
+
// Interception
|
|
65
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
const isDOMTag = (type: unknown): type is string => typeof type === 'string' && /^[a-z]/.test(type)
|
|
68
|
+
|
|
69
|
+
type ElementCreator = (type: unknown, props: unknown, ...rest: unknown[]) => ReactElement
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Creates an intercepted version of an element creation function.
|
|
73
|
+
* Works with both createElement (children as rest params) and jsx/jsxs (children in props).
|
|
74
|
+
*/
|
|
75
|
+
function createInterceptor(
|
|
76
|
+
original: ElementCreator,
|
|
77
|
+
listeners: CreateElementListener[],
|
|
78
|
+
getNextId: () => string,
|
|
79
|
+
store: ExtractorStore,
|
|
80
|
+
): ElementCreator {
|
|
81
|
+
return (type, elementProps, ...rest) => {
|
|
82
|
+
if (!isDOMTag(type)) {
|
|
83
|
+
return original(type, elementProps, ...rest)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const props = (elementProps ?? {}) as Record<string, unknown>
|
|
87
|
+
const traceId = getNextId()
|
|
88
|
+
|
|
89
|
+
// Children location differs: createElement uses rest params, jsx/jsxs uses props.children
|
|
90
|
+
const children = rest.length > 0 ? rest : props.children ? [props.children] : []
|
|
91
|
+
|
|
92
|
+
const event: CreateElementEvent = {
|
|
93
|
+
tag: type,
|
|
94
|
+
props: { ...props, children },
|
|
95
|
+
traceId,
|
|
96
|
+
children: children as unknown[],
|
|
97
|
+
store,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
listeners.forEach((listener) => listener.onCreateElement(event))
|
|
101
|
+
|
|
102
|
+
const stampedProps = { ...props, [TRACE_ATTR]: traceId }
|
|
103
|
+
return original(type, stampedProps, ...rest)
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
108
|
+
// Renderer
|
|
109
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Renders a React component to static HTML while intercepting element creation.
|
|
113
|
+
*
|
|
114
|
+
* Uses monkey-patching of React.createElement and jsx/jsxs to notify listeners
|
|
115
|
+
* when DOM elements are created. Each DOM element gets a unique `data-trace-id`
|
|
116
|
+
* attribute for tracking.
|
|
117
|
+
*
|
|
118
|
+
* @param Component - The React component to render
|
|
119
|
+
* @param componentProps - Props to pass to the component
|
|
120
|
+
* @param listeners - Listeners to notify on each DOM element creation
|
|
121
|
+
* @param store - Shared ExtractorStore, included in each CreateElementEvent
|
|
122
|
+
* @returns Static HTML string with trace IDs on DOM elements
|
|
123
|
+
*
|
|
124
|
+
* @example
|
|
125
|
+
* ```ts
|
|
126
|
+
* const store = new ExtractorStore()
|
|
127
|
+
* const tracker = createPropTracker(getSpyMetadata)
|
|
128
|
+
* const html = renderWithExtractors(MyComponent, { title: 'Hello' }, [tracker], store)
|
|
129
|
+
* // html contains: <div data-trace-id="t1">...</div>
|
|
130
|
+
* // tracker.stores now has prop→DOM bindings
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
export function renderWithExtractors(
|
|
134
|
+
Component: ComponentType<unknown>,
|
|
135
|
+
componentProps: unknown,
|
|
136
|
+
listeners: CreateElementListener[],
|
|
137
|
+
store: ExtractorStore,
|
|
138
|
+
): string {
|
|
139
|
+
let nextId = 0
|
|
140
|
+
const getNextId = () => `t${++nextId}`
|
|
141
|
+
|
|
142
|
+
// Store originals
|
|
143
|
+
const originalCreateElement = React.createElement
|
|
144
|
+
const { jsx: originalJsx, jsxs: originalJsxs, jsxDEV: originalJsxDEV } = getOriginals()
|
|
145
|
+
|
|
146
|
+
// Obtain real CJS module objects so we can patch them for pre-built CJS consumers
|
|
147
|
+
const cjsRequire = createRequire(import.meta.url)
|
|
148
|
+
const cjsRuntime = cjsRequire('react/jsx-runtime') as Record<string, unknown>
|
|
149
|
+
const cjsDevRuntime = cjsRequire('react/jsx-dev-runtime') as Record<string, unknown>
|
|
150
|
+
|
|
151
|
+
// Save the original CJS exports
|
|
152
|
+
const origCjsJsx = cjsRuntime.jsx
|
|
153
|
+
const origCjsJsxs = cjsRuntime.jsxs
|
|
154
|
+
const origCjsJsxDEV = cjsDevRuntime.jsxDEV
|
|
155
|
+
|
|
156
|
+
// Create interceptors
|
|
157
|
+
const interceptedCreateElement = createInterceptor(
|
|
158
|
+
originalCreateElement as ElementCreator,
|
|
159
|
+
listeners,
|
|
160
|
+
getNextId,
|
|
161
|
+
store,
|
|
162
|
+
)
|
|
163
|
+
const interceptedJsx = createInterceptor(originalJsx as ElementCreator, listeners, getNextId, store)
|
|
164
|
+
const interceptedJsxs = createInterceptor(originalJsxs as ElementCreator, listeners, getNextId, store)
|
|
165
|
+
const interceptedJsxDEV = createInterceptor(originalJsxDEV as ElementCreator, listeners, getNextId, store)
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
// @ts-expect-error Monkey-patch createElement
|
|
169
|
+
React.createElement = interceptedCreateElement
|
|
170
|
+
// Use the interceptor API for jsx-runtime (includes jsxDEV for dev mode)
|
|
171
|
+
setJsxInterceptors(
|
|
172
|
+
interceptedJsx as typeof originalJsx,
|
|
173
|
+
interceptedJsxs as typeof originalJsxs,
|
|
174
|
+
interceptedJsxDEV as typeof originalJsxDEV,
|
|
175
|
+
)
|
|
176
|
+
// Patch real CJS module exports for pre-built CJS bundles
|
|
177
|
+
cjsRuntime.jsx = interceptedJsx
|
|
178
|
+
cjsRuntime.jsxs = interceptedJsxs
|
|
179
|
+
cjsDevRuntime.jsxDEV = interceptedJsxDEV
|
|
180
|
+
|
|
181
|
+
const element = React.createElement(Component, componentProps as Record<string, unknown>)
|
|
182
|
+
return renderToStaticMarkup(element)
|
|
183
|
+
} finally {
|
|
184
|
+
// Restore originals
|
|
185
|
+
React.createElement = originalCreateElement
|
|
186
|
+
setJsxInterceptors() // Restores to originals
|
|
187
|
+
// Restore CJS module exports
|
|
188
|
+
cjsRuntime.jsx = origCjsJsx
|
|
189
|
+
cjsRuntime.jsxs = origCjsJsxs
|
|
190
|
+
cjsDevRuntime.jsxDEV = origCjsJsxDEV
|
|
191
|
+
}
|
|
192
|
+
}
|