@wix/zero-config-implementation 1.77.0 → 1.79.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 (35) hide show
  1. package/README.md +4 -0
  2. package/dist/{index-DRWoSsTZ.js → index-CoU1WRYf.js} +28022 -28645
  3. package/dist/{index-CuKmaUAA.js → index-DCJue8M7.js} +1 -1
  4. package/dist/index.js +1 -1
  5. package/package.json +3 -3
  6. package/src/component-renderer.ts +90 -0
  7. package/src/converters/to-editor-component.ts +2 -2
  8. package/src/extensions/context-providers/context.test.ts +181 -0
  9. package/src/extensions/context-providers/context.ts +83 -0
  10. package/src/extensions/context-providers/mock-provider.test.ts +44 -0
  11. package/src/extensions/context-providers/mock-provider.ts +210 -0
  12. package/src/extensions/context-providers/types.ts +22 -0
  13. package/src/{ref-elements → extensions/ref-elements}/context.test.ts +4 -4
  14. package/src/extensions/ref-elements/context.ts +168 -0
  15. package/src/{ref-elements → extensions/ref-elements}/eligible-paths.ts +1 -1
  16. package/src/extensions/shared/ast-scanner.ts +85 -0
  17. package/src/extensions/shared/catalog-loader.ts +64 -0
  18. package/src/{ref-elements → extensions/shared}/module-resolution.test.ts +2 -2
  19. package/src/index.ts +108 -67
  20. package/src/information-extractors/react/extractors/prop-tracker.test.ts +2 -2
  21. package/src/information-extractors/react/extractors/prop-tracker.ts +3 -3
  22. package/src/information-extractors/react/utils/mock-generator.test.ts +107 -3
  23. package/src/information-extractors/react/utils/mock-generator.ts +104 -1
  24. package/src/manifest-pipeline.ts +4 -1
  25. package/src/module-loader.test.ts +1 -1
  26. package/src/module-loader.ts +1 -1
  27. package/src/react-runtime-loader.ts +49 -8
  28. package/dist/index-BGHyk7CU.js +0 -708
  29. package/src/ref-elements/context.ts +0 -280
  30. /package/src/{ref-elements → extensions/ref-elements}/component-tag.ts +0 -0
  31. /package/src/{ref-elements → extensions/ref-elements}/path-utils.test.ts +0 -0
  32. /package/src/{ref-elements → extensions/ref-elements}/path-utils.ts +0 -0
  33. /package/src/{ref-elements → extensions/ref-elements}/types.ts +0 -0
  34. /package/src/{ref-elements → extensions/shared}/module-resolution.ts +0 -0
  35. /package/src/{ref-elements/module-specifier.ts → extensions/shared/package-specifier.ts} +0 -0
@@ -1,4 +1,4 @@
1
- import { g as x } from "./index-DRWoSsTZ.js";
1
+ import { g as x } from "./index-CoU1WRYf.js";
2
2
  function h(r, a) {
3
3
  for (var i = 0; i < a.length; i++) {
4
4
  const o = a[i];
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-DRWoSsTZ.js";
1
+ import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-CoU1WRYf.js";
2
2
  import "react";
3
3
  export {
4
4
  t as BaseError,
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.77.0",
7
+ "version": "1.79.0",
8
8
  "description": "Core library for extracting component manifests from JS and CSS files",
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
@@ -39,7 +39,7 @@
39
39
  }
40
40
  },
41
41
  "dependencies": {
42
- "@wix/builder-services-wrapper": "^1.42.0",
42
+ "@wix/builder-services-wrapper": "^1.56.0",
43
43
  "@wix/react-component-schema": "1.7.0"
44
44
  },
45
45
  "devDependencies": {
@@ -82,5 +82,5 @@
82
82
  ]
83
83
  }
84
84
  },
85
- "falconPackageHash": "29046e5eac674ef42dc21f8449f7f0b5c99691e835215a1133464500"
85
+ "falconPackageHash": "95a755e580164c3acabbf27f3813514e4fcdb56b90214e7b830134af"
86
86
  }
@@ -76,6 +76,7 @@ import {
76
76
  setCreateElementInterceptor,
77
77
  setJsxInterceptors,
78
78
  } from './react-runtime-interceptor'
79
+ import { getActiveReactModule, setActiveReactModule } from './react-runtime-loader'
79
80
 
80
81
  export const TRACE_ATTR = 'data-trace-id'
81
82
 
@@ -132,6 +133,84 @@ function createInterceptor(
132
133
  }
133
134
  }
134
135
 
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+ // Dispatcher bridge
138
+ // ─────────────────────────────────────────────────────────────────────────────
139
+
140
+ const REACT_18_INTERNALS_KEY = '__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED'
141
+ const REACT_19_INTERNALS_KEY = '__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE'
142
+
143
+ function readReactInternals(reactModule: unknown): Record<string, unknown> | undefined {
144
+ if (typeof reactModule !== 'object' || reactModule === null) {
145
+ return undefined
146
+ }
147
+
148
+ const reactRecord = reactModule as Record<string, unknown>
149
+ const internals = reactRecord[REACT_18_INTERNALS_KEY] ?? reactRecord[REACT_19_INTERNALS_KEY]
150
+ return typeof internals === 'object' && internals !== null ? (internals as Record<string, unknown>) : undefined
151
+ }
152
+
153
+ /**
154
+ * Forwards the host React's hook dispatcher to the user React so host-bound hook
155
+ * callers (e.g. WixServicesWrapper) resolve the dispatcher the user's react-dom
156
+ * installs during SSR. Returns a function that restores the host React.
157
+ */
158
+ function bridgeHostDispatcherToUserReact(hostReact: unknown, userReact: unknown): () => void {
159
+ const hostInternals = readReactInternals(hostReact)
160
+ const userInternals = readReactInternals(userReact)
161
+ if (!hostInternals || !userInternals) {
162
+ return () => {}
163
+ }
164
+
165
+ // React 18 exposes a `{ current }` holder captured by reference at load time, so
166
+ // forward its `current` to the user holder rather than swapping the holder itself.
167
+ if (hostInternals.ReactCurrentDispatcher && userInternals.ReactCurrentDispatcher) {
168
+ const hostDispatcherHolder = hostInternals.ReactCurrentDispatcher as Record<string, unknown>
169
+ const userDispatcherHolder = userInternals.ReactCurrentDispatcher as Record<string, unknown>
170
+ if (hostDispatcherHolder === userDispatcherHolder) {
171
+ return () => {}
172
+ }
173
+
174
+ const previousDescriptor = Object.getOwnPropertyDescriptor(hostDispatcherHolder, 'current')
175
+ Object.defineProperty(hostDispatcherHolder, 'current', {
176
+ configurable: true,
177
+ get: () => userDispatcherHolder.current,
178
+ set: (dispatcher) => {
179
+ userDispatcherHolder.current = dispatcher
180
+ },
181
+ })
182
+ return () => {
183
+ Object.defineProperty(
184
+ hostDispatcherHolder,
185
+ 'current',
186
+ previousDescriptor ?? { configurable: true, enumerable: true, value: null, writable: true },
187
+ )
188
+ }
189
+ }
190
+
191
+ // React 19 keeps the dispatcher directly on the internals object as `H`, so
192
+ // forward that property to the user internals.
193
+ if ('H' in userInternals) {
194
+ const previousDescriptor = Object.getOwnPropertyDescriptor(hostInternals, 'H')
195
+ Object.defineProperty(hostInternals, 'H', {
196
+ configurable: true,
197
+ get: () => userInternals.H,
198
+ set: (dispatcher) => {
199
+ userInternals.H = dispatcher
200
+ },
201
+ })
202
+ return () => {
203
+ Object.defineProperty(
204
+ hostInternals,
205
+ 'H',
206
+ previousDescriptor ?? { configurable: true, enumerable: true, value: undefined, writable: true },
207
+ )
208
+ }
209
+ }
210
+
211
+ return () => {}
212
+ }
213
+
135
214
  // ─────────────────────────────────────────────────────────────────────────────
136
215
  // Renderer
137
216
  // ─────────────────────────────────────────────────────────────────────────────
@@ -248,6 +327,12 @@ export function renderWithExtractors(
248
327
  const interceptedJsxs = createInterceptor(originalJsxs as ElementCreator, listeners, getNextId, store)
249
328
  const interceptedJsxDEV = createInterceptor(originalJsxDEV as ElementCreator, listeners, getNextId, store)
250
329
 
330
+ // The shim reads globalThis[REACT_KEY] on every hook call, so pointing it at
331
+ // userReact ensures ESM hooks use the same dispatcher that SSR drives.
332
+ const previousActiveReactModule = getActiveReactModule()
333
+ // Restores the host React's hook dispatcher, bridged to the user React below.
334
+ let restoreHostDispatcher: () => void = () => {}
335
+
251
336
  try {
252
337
  // @ts-expect-error Monkey-patch createElement for default-import callers
253
338
  React.createElement = interceptedCreateElement
@@ -268,6 +353,9 @@ export function renderWithExtractors(
268
353
  // Patch user-bundle React modules when they are distinct instances
269
354
  if (userReact) {
270
355
  userReact.createElement = interceptedCreateElement
356
+ setActiveReactModule(userReact)
357
+ // Host-bound hook callers (e.g. WixServicesWrapper) need the user React's dispatcher too.
358
+ restoreHostDispatcher = bridgeHostDispatcherToUserReact(React, userReact)
271
359
  }
272
360
  if (userJsxRuntime) {
273
361
  userJsxRuntime.jsx = interceptedJsx
@@ -281,6 +369,8 @@ export function renderWithExtractors(
281
369
  const element = renderingReact.createElement(Component, componentProps as Record<string, unknown>)
282
370
  return (userRenderToStaticMarkup ?? renderToStaticMarkup)(element)
283
371
  } finally {
372
+ restoreHostDispatcher()
373
+ setActiveReactModule(previousActiveReactModule)
284
374
  // Restore originals
285
375
  // @ts-expect-error Restore monkey-patched createElement
286
376
  React.createElement = originalCreateElement
@@ -11,6 +11,8 @@ import type {
11
11
  } from '@wix/react-component-schema'
12
12
  import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
13
13
  import { camelCase } from 'case-anything'
14
+ import { buildRefElementManifestKey } from '../extensions/ref-elements/path-utils'
15
+ import type { RefElementMatch } from '../extensions/ref-elements/types'
14
16
  import type { ComponentInfoWithCss } from '../index'
15
17
  import type { MatchedCssData, RegisteredCustomProperty } from '../information-extractors/css/types'
16
18
  import type {
@@ -21,8 +23,6 @@ import type {
21
23
  TrackingStores,
22
24
  } from '../information-extractors/react'
23
25
  import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
24
- import { buildRefElementManifestKey } from '../ref-elements/path-utils'
25
- import type { RefElementMatch } from '../ref-elements/types'
26
26
  import { findPreferredSemanticClass, normalizeClassNames } from '../utils/css-class'
27
27
  import { resolveLonghand } from './css-longhand-resolver'
28
28
  import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
@@ -0,0 +1,181 @@
1
+ import fs from 'node:fs'
2
+ import os from 'node:os'
3
+ import path from 'node:path'
4
+ import ts from 'typescript'
5
+ import { afterEach, describe, expect, it } from 'vitest'
6
+
7
+ import { resolveModuleUrlFromEntryPath } from '../shared/module-resolution'
8
+ import { buildContextProviderModules } from './context'
9
+
10
+ const temporaryDirectoryPaths: string[] = []
11
+
12
+ afterEach(() => {
13
+ for (const temporaryDirectoryPath of temporaryDirectoryPaths.splice(0)) {
14
+ fs.rmSync(temporaryDirectoryPath, { force: true, recursive: true })
15
+ }
16
+ })
17
+
18
+ const FORMAT_A_CATALOG_ENTRY = {
19
+ type: 'GenericExtension',
20
+ options: {
21
+ compType: 'EDITOR_CONTEXT_PROVIDER',
22
+ compData: {
23
+ editorContextProvider: {
24
+ context: { items: { title: { dataType: 'text' } } },
25
+ resources: {
26
+ contextSpecifier: {
27
+ hook: 'useEventContext',
28
+ moduleSpecifier: 'my-context-package/context-provider',
29
+ },
30
+ dependencies: { contextDependencies: [] },
31
+ },
32
+ },
33
+ },
34
+ },
35
+ }
36
+
37
+ const FORMAT_B_CATALOG_ENTRY = {
38
+ context: { items: { title: { dataType: 'text' } } },
39
+ resources: {
40
+ contextSpecifier: {
41
+ hook: 'useEventContext',
42
+ moduleSpecifier: 'my-context-package/context-provider',
43
+ },
44
+ dependencies: { contextDependencies: [] },
45
+ },
46
+ }
47
+
48
+ function createPackageSetup(options: {
49
+ extensionsContent: string
50
+ importSpecifier?: string
51
+ }): { compiledEntryPath: string; sourceFilePath: string } {
52
+ const temporaryDirectoryPath = fs.mkdtempSync(path.join(os.tmpdir(), 'zero-config-context-provider-'))
53
+ temporaryDirectoryPaths.push(temporaryDirectoryPath)
54
+
55
+ const sourceFilePath = path.join(temporaryDirectoryPath, 'Component.tsx')
56
+ const compiledEntryPath = path.join(temporaryDirectoryPath, 'dist', 'index.js')
57
+ const packageDirectoryPath = path.join(temporaryDirectoryPath, 'node_modules', 'my-context-package')
58
+
59
+ fs.mkdirSync(path.dirname(compiledEntryPath), { recursive: true })
60
+ fs.mkdirSync(packageDirectoryPath, { recursive: true })
61
+
62
+ fs.writeFileSync(
63
+ sourceFilePath,
64
+ `import { useEventContext } from '${options.importSpecifier ?? 'my-context-package/context-provider'}'\nexport default function Component() { return null }\n`,
65
+ )
66
+ fs.writeFileSync(compiledEntryPath, 'export {}\n')
67
+ fs.writeFileSync(path.join(packageDirectoryPath, 'context-provider.js'), 'export function useEventContext() {}\n')
68
+ fs.writeFileSync(path.join(packageDirectoryPath, 'extensions.js'), options.extensionsContent)
69
+ fs.writeFileSync(
70
+ path.join(packageDirectoryPath, 'package.json'),
71
+ JSON.stringify(
72
+ {
73
+ name: 'my-context-package',
74
+ type: 'module',
75
+ exports: {
76
+ '.': './context-provider.js',
77
+ './context-provider': './context-provider.js',
78
+ './extensions': './extensions.js',
79
+ },
80
+ },
81
+ null,
82
+ 2,
83
+ ),
84
+ )
85
+
86
+ return { compiledEntryPath, sourceFilePath }
87
+ }
88
+
89
+ function createProgram(sourceFilePath: string): ts.Program {
90
+ return ts.createProgram({
91
+ rootNames: [sourceFilePath],
92
+ options: {
93
+ allowJs: true,
94
+ jsx: ts.JsxEmit.ReactJSX,
95
+ },
96
+ })
97
+ }
98
+
99
+ describe('buildContextProviderModules', () => {
100
+ it('returns empty when the import is a bare package with no sub-path', async () => {
101
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
102
+ importSpecifier: 'my-context-package',
103
+ extensionsContent: `export default [${JSON.stringify(FORMAT_A_CATALOG_ENTRY)}]\n`,
104
+ })
105
+ const program = createProgram(sourceFilePath)
106
+
107
+ const modules = await buildContextProviderModules(program, sourceFilePath, compiledEntryPath)
108
+
109
+ expect(modules).toEqual([])
110
+ })
111
+
112
+ it('resolves a context provider from a Format A catalog entry (manifest nested under options.compData.editorContextProvider)', async () => {
113
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
114
+ extensionsContent: `export default [${JSON.stringify(FORMAT_A_CATALOG_ENTRY)}]\n`,
115
+ })
116
+ const program = createProgram(sourceFilePath)
117
+
118
+ const modules = await buildContextProviderModules(program, sourceFilePath, compiledEntryPath)
119
+
120
+ expect(modules).toEqual([
121
+ {
122
+ hookName: 'useEventContext',
123
+ moduleSpecifier: 'my-context-package/context-provider',
124
+ resolvedModuleUrl: resolveModuleUrlFromEntryPath(compiledEntryPath, 'my-context-package/context-provider'),
125
+ contextSchema: { items: { title: { dataType: 'text' } } },
126
+ contextDependencies: [],
127
+ },
128
+ ])
129
+ })
130
+
131
+ it('resolves a context provider from a Format B catalog entry (manifest at root)', async () => {
132
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
133
+ extensionsContent: `export default [${JSON.stringify(FORMAT_B_CATALOG_ENTRY)}]\n`,
134
+ })
135
+ const program = createProgram(sourceFilePath)
136
+
137
+ const modules = await buildContextProviderModules(program, sourceFilePath, compiledEntryPath)
138
+
139
+ expect(modules).toEqual([
140
+ {
141
+ hookName: 'useEventContext',
142
+ moduleSpecifier: 'my-context-package/context-provider',
143
+ resolvedModuleUrl: resolveModuleUrlFromEntryPath(compiledEntryPath, 'my-context-package/context-provider'),
144
+ contextSchema: { items: { title: { dataType: 'text' } } },
145
+ contextDependencies: [],
146
+ },
147
+ ])
148
+ })
149
+
150
+ it('returns empty when the imported specifier does not match any catalog entry moduleSpecifier', async () => {
151
+ const nonMatchingEntry = {
152
+ ...FORMAT_B_CATALOG_ENTRY,
153
+ resources: {
154
+ ...FORMAT_B_CATALOG_ENTRY.resources,
155
+ contextSpecifier: {
156
+ hook: 'useOtherContext',
157
+ moduleSpecifier: 'my-context-package/other-module',
158
+ },
159
+ },
160
+ }
161
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
162
+ extensionsContent: `export default [${JSON.stringify(nonMatchingEntry)}]\n`,
163
+ })
164
+ const program = createProgram(sourceFilePath)
165
+
166
+ const modules = await buildContextProviderModules(program, sourceFilePath, compiledEntryPath)
167
+
168
+ expect(modules).toEqual([])
169
+ })
170
+
171
+ it('throws when the extensions catalog throws during loading', async () => {
172
+ const { compiledEntryPath, sourceFilePath } = createPackageSetup({
173
+ extensionsContent: "throw new Error('broken catalog')\n",
174
+ })
175
+ const program = createProgram(sourceFilePath)
176
+
177
+ await expect(buildContextProviderModules(program, sourceFilePath, compiledEntryPath)).rejects.toThrow(
178
+ 'broken catalog',
179
+ )
180
+ })
181
+ })
@@ -0,0 +1,83 @@
1
+ import type ts from 'typescript'
2
+ import { collectAllPackageImportSpecifiers } from '../shared/ast-scanner'
3
+ import { loadNormalizedCatalogEntries } from '../shared/catalog-loader'
4
+ import { resolveModuleUrlFromEntryPath } from '../shared/module-resolution'
5
+ import type { ContextProviderModule, ContextSchema } from './types'
6
+
7
+ interface NormalizedContextProviderCatalogEntry {
8
+ hookName: string
9
+ moduleSpecifier: string
10
+ contextSchema: ContextSchema
11
+ contextDependencies: string[]
12
+ }
13
+
14
+ export async function buildContextProviderModules(
15
+ program: ts.Program,
16
+ sourceFilePath: string,
17
+ compiledEntryPath: string,
18
+ ): Promise<ContextProviderModule[]> {
19
+ const allImportedSpecifiers = collectAllPackageImportSpecifiers(program, sourceFilePath)
20
+ if (allImportedSpecifiers.length === 0) {
21
+ return []
22
+ }
23
+
24
+ const catalogEntries = await loadNormalizedCatalogEntries(
25
+ allImportedSpecifiers,
26
+ compiledEntryPath,
27
+ normalizeContextProviderEntry,
28
+ )
29
+
30
+ const importedSpecifierSet = new Set(allImportedSpecifiers)
31
+ const contextProviderModules: ContextProviderModule[] = []
32
+
33
+ for (const catalogEntry of catalogEntries) {
34
+ if (!importedSpecifierSet.has(catalogEntry.moduleSpecifier)) {
35
+ continue
36
+ }
37
+
38
+ const resolvedModuleUrl = resolveModuleUrlFromEntryPath(compiledEntryPath, catalogEntry.moduleSpecifier)
39
+ if (!resolvedModuleUrl) {
40
+ continue
41
+ }
42
+
43
+ contextProviderModules.push({
44
+ hookName: catalogEntry.hookName,
45
+ moduleSpecifier: catalogEntry.moduleSpecifier,
46
+ resolvedModuleUrl,
47
+ contextSchema: catalogEntry.contextSchema,
48
+ contextDependencies: catalogEntry.contextDependencies,
49
+ })
50
+ }
51
+
52
+ return contextProviderModules
53
+ }
54
+
55
+ type UnknownObject = Record<string, unknown> | undefined
56
+
57
+ function normalizeContextProviderEntry(rawEntry: unknown): NormalizedContextProviderCatalogEntry | undefined {
58
+ if (rawEntry === null || typeof rawEntry !== 'object') {
59
+ return undefined
60
+ }
61
+
62
+ const entry = rawEntry as Record<string, unknown>
63
+ // Format A wraps the manifest under options.compData.editorContextProvider; Format B exports it at the root.
64
+ const compData = (entry.options as UnknownObject)?.compData as UnknownObject
65
+ const normalizedEntry = (compData?.editorContextProvider ?? entry) as Record<string, unknown>
66
+
67
+ const resources = normalizedEntry.resources as UnknownObject
68
+ const contextSpecifier = resources?.contextSpecifier as { hook?: string; moduleSpecifier?: string } | undefined
69
+
70
+ if (!contextSpecifier?.hook || !contextSpecifier?.moduleSpecifier) {
71
+ return undefined
72
+ }
73
+
74
+ const dependencies = resources?.dependencies as { contextDependencies?: string[] } | undefined
75
+ return {
76
+ hookName: contextSpecifier.hook,
77
+ moduleSpecifier: contextSpecifier.moduleSpecifier,
78
+ contextSchema: {
79
+ items: (normalizedEntry.context as ContextSchema | undefined)?.items ?? {},
80
+ },
81
+ contextDependencies: dependencies?.contextDependencies ?? [],
82
+ }
83
+ }
@@ -0,0 +1,44 @@
1
+ import React, { type ComponentType } from 'react'
2
+ import { renderToStaticMarkup } from 'react-dom/server'
3
+ import { describe, expect, it } from 'vitest'
4
+ import { buildContextAwareWrapper } from './mock-provider'
5
+
6
+ function makeProvider(label: string): ComponentType<{ children?: React.ReactNode }> {
7
+ const Provider = ({ children }: { children?: React.ReactNode }) =>
8
+ React.createElement('div', { 'data-provider': label }, children)
9
+ return Provider
10
+ }
11
+
12
+ describe('buildContextAwareWrapper', () => {
13
+ it('places dependency providers outermost (topological sort)', () => {
14
+ // ticketProvider depends on eventProvider — eventProvider must render outermost
15
+ const loadedProviders = [
16
+ { moduleSpecifier: 'pkg/ticket', contextDependencies: ['pkg/event'], Provider: makeProvider('ticket') },
17
+ { moduleSpecifier: 'pkg/event', contextDependencies: [], Provider: makeProvider('event') },
18
+ ]
19
+
20
+ const identityWrapper = (Component: ComponentType<unknown>): ComponentType<unknown> => Component
21
+ const wrap = buildContextAwareWrapper(loadedProviders, identityWrapper, React)
22
+ const Inner: ComponentType<unknown> = () => React.createElement('span', null, 'content')
23
+ const html = renderToStaticMarkup(React.createElement(wrap(Inner), {}))
24
+
25
+ // event (the dependency) must appear before ticket in the HTML — it's the outer wrapper
26
+ expect(html.indexOf('data-provider="event"')).toBeLessThan(html.indexOf('data-provider="ticket"'))
27
+ })
28
+
29
+ it('composes options.wrapper as the outermost layer when provided', () => {
30
+ const customWrapper =
31
+ (Component: ComponentType<unknown>): ComponentType<unknown> =>
32
+ (props) =>
33
+ React.createElement('div', { 'data-wrapper': 'custom' }, React.createElement(Component, props as object))
34
+
35
+ const loadedProviders = [{ moduleSpecifier: 'pkg/event', contextDependencies: [], Provider: makeProvider('event') }]
36
+
37
+ const wrap = buildContextAwareWrapper(loadedProviders, customWrapper, React)
38
+ const Inner: ComponentType<unknown> = () => React.createElement('span', null, 'content')
39
+ const html = renderToStaticMarkup(React.createElement(wrap(Inner), {}))
40
+
41
+ expect(html.indexOf('data-wrapper="custom"')).toBeLessThan(html.indexOf('data-provider="event"'))
42
+ expect(html.indexOf('data-provider="event"')).toBeLessThan(html.indexOf('<span>'))
43
+ })
44
+ })