@wix/zero-config-implementation 1.85.0 → 1.87.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/dist/index.d.ts +1 -0
- package/dist/index.js +82644 -30
- package/package.json +28 -4
- package/src/converters/active-item-index-builder.test.ts +149 -0
- package/src/converters/active-item-index-builder.ts +90 -0
- package/src/converters/to-editor-component.test.ts +64 -0
- package/src/converters/to-editor-component.ts +10 -0
- package/src/converters/utils.test.ts +14 -1
- package/src/converters/utils.ts +11 -0
- package/src/extensions/wix-services/service-stub.test.ts +149 -0
- package/src/extensions/wix-services/service-stub.ts +126 -0
- package/src/extensions/wix-services/wrapper.test.ts +63 -0
- package/src/extensions/wix-services/wrapper.ts +204 -0
- package/src/index.ts +2 -6
- package/src/information-extractors/ts/components.test.ts +37 -0
- package/src/information-extractors/ts/components.ts +74 -2
- package/src/information-extractors/ts/types.ts +5 -0
- package/dist/index-CQq7GOf9.js +0 -96
- package/dist/index-DBSnnglR.js +0 -100296
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import { createServicesManager } from '@wix/services-manager'
|
|
2
|
+
import type { ServicesManager, ServicesRegistrar } from '@wix/services-manager/types'
|
|
3
|
+
|
|
4
|
+
/** Definitions are plain strings at runtime, so one doubles as both registry key and service id. */
|
|
5
|
+
type ServiceDefinitionLike = { toString(): string }
|
|
6
|
+
|
|
7
|
+
/** A thenable stub would hang `await`; a stub with `$$typeof` would be read as a React element. */
|
|
8
|
+
const NEVER_STUBBED_PROPERTIES: ReadonlySet<PropertyKey> = new Set([
|
|
9
|
+
'then',
|
|
10
|
+
'catch',
|
|
11
|
+
'finally',
|
|
12
|
+
'$$typeof',
|
|
13
|
+
'toJSON',
|
|
14
|
+
'nodeType',
|
|
15
|
+
])
|
|
16
|
+
|
|
17
|
+
const HANDLED_SYMBOLS: ReadonlySet<symbol> = new Set([Symbol.toPrimitive, Symbol.toStringTag, Symbol.iterator])
|
|
18
|
+
|
|
19
|
+
// Shared, so `stub.toString === stub.toString` holds.
|
|
20
|
+
const readAsEmptyString = (): string => ''
|
|
21
|
+
function* iterateAsEmpty(): Generator<never> {}
|
|
22
|
+
|
|
23
|
+
// Not `Symbol.for`, whose key is global and could collide with another library's.
|
|
24
|
+
const CALL_RESULT_KEY = Symbol('call-result')
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Inert but still truthy — a component branching on a stubbed flag takes the truthy path, which
|
|
28
|
+
* is why stubbed services are reported to the caller.
|
|
29
|
+
*/
|
|
30
|
+
function createServiceStub(servicePath: string): unknown {
|
|
31
|
+
const childStubs = new Map<PropertyKey, unknown>()
|
|
32
|
+
|
|
33
|
+
// Arrow function: callable, and owns no non-configurable props that would make the
|
|
34
|
+
// `ownKeys`/`getOwnPropertyDescriptor` traps below violate the Proxy invariants.
|
|
35
|
+
const callableTarget = (): void => {}
|
|
36
|
+
|
|
37
|
+
return new Proxy(callableTarget, {
|
|
38
|
+
get(_target, property) {
|
|
39
|
+
if (NEVER_STUBBED_PROPERTIES.has(property)) {
|
|
40
|
+
return undefined
|
|
41
|
+
}
|
|
42
|
+
if (property === Symbol.toPrimitive || property === 'valueOf' || property === 'toString') {
|
|
43
|
+
return readAsEmptyString
|
|
44
|
+
}
|
|
45
|
+
if (property === Symbol.toStringTag) {
|
|
46
|
+
return 'WixServiceStub'
|
|
47
|
+
}
|
|
48
|
+
if (property === Symbol.iterator) {
|
|
49
|
+
return iterateAsEmpty
|
|
50
|
+
}
|
|
51
|
+
if (typeof property === 'symbol') {
|
|
52
|
+
return undefined
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const existingChildStub = childStubs.get(property)
|
|
56
|
+
if (existingChildStub !== undefined) {
|
|
57
|
+
return existingChildStub
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const childStub = createServiceStub(`${servicePath}.${property}`)
|
|
61
|
+
childStubs.set(property, childStub)
|
|
62
|
+
return childStub
|
|
63
|
+
},
|
|
64
|
+
|
|
65
|
+
apply() {
|
|
66
|
+
const existingCallResult = childStubs.get(CALL_RESULT_KEY)
|
|
67
|
+
if (existingCallResult !== undefined) {
|
|
68
|
+
return existingCallResult
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const callResult = createServiceStub(`${servicePath}()`)
|
|
72
|
+
childStubs.set(CALL_RESULT_KEY, callResult)
|
|
73
|
+
return callResult
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
// Mirrors `get`, so `in` never reports a property that reads back as undefined.
|
|
77
|
+
has: (_target, property) => {
|
|
78
|
+
if (NEVER_STUBBED_PROPERTIES.has(property)) {
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
return typeof property === 'symbol' ? HANDLED_SYMBOLS.has(property) : true
|
|
82
|
+
},
|
|
83
|
+
set: () => true,
|
|
84
|
+
// No own keys, so spreading terminates instead of recursing forever.
|
|
85
|
+
ownKeys: () => [],
|
|
86
|
+
getOwnPropertyDescriptor: () => undefined,
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export interface AutoStubFallbackOptions {
|
|
91
|
+
/** Invoked once per distinct service id. */
|
|
92
|
+
onServiceStubbed?: (serviceId: string) => void
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* `createServicesManager` delegates to its `parentServicesManager` for unbound definitions, so
|
|
97
|
+
* passing this as the parent turns a "Service X is not provided" throw into an inert stub.
|
|
98
|
+
*/
|
|
99
|
+
export function createAutoStubFallbackManager(options: AutoStubFallbackOptions = {}): ServicesManager {
|
|
100
|
+
const stubsByServiceId = new Map<string, unknown>()
|
|
101
|
+
|
|
102
|
+
const getService = (definition: ServiceDefinitionLike): never => {
|
|
103
|
+
const serviceId = definition.toString()
|
|
104
|
+
|
|
105
|
+
const existingStub = stubsByServiceId.get(serviceId)
|
|
106
|
+
if (existingStub !== undefined) {
|
|
107
|
+
return existingStub as never
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const stub = createServiceStub(serviceId)
|
|
111
|
+
stubsByServiceId.set(serviceId, stub)
|
|
112
|
+
options.onServiceStubbed?.(serviceId)
|
|
113
|
+
return stub as never
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const fallbackManager: ServicesManager = {
|
|
117
|
+
getService: getService as ServicesManager['getService'],
|
|
118
|
+
hasService: () => true,
|
|
119
|
+
addService: () => {},
|
|
120
|
+
addServices: () => {},
|
|
121
|
+
dispose: () => {},
|
|
122
|
+
extend: (servicesMap: ServicesRegistrar) => createServicesManager(servicesMap, fallbackManager),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return fallbackManager
|
|
126
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { useService } from '@wix/services-manager-react'
|
|
2
|
+
import type { ServiceDefinition } from '@wix/services-manager/types'
|
|
3
|
+
import { TranslationsDefinition } from '@wix/viewer-service-translations/definition'
|
|
4
|
+
import React, { type ComponentType } from 'react'
|
|
5
|
+
import { renderToStaticMarkup } from 'react-dom/server'
|
|
6
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
7
|
+
import { wrapWithWixServices } from './wrapper'
|
|
8
|
+
|
|
9
|
+
const UnregisteredDefinition = '@wix/example-unregistered-analytics' as unknown as ServiceDefinition<
|
|
10
|
+
{ trackingId: string },
|
|
11
|
+
never
|
|
12
|
+
>
|
|
13
|
+
|
|
14
|
+
function renderThroughWrapper(Component: ComponentType<unknown>): string {
|
|
15
|
+
return renderToStaticMarkup(React.createElement(wrapWithWixServices(Component), {}))
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
describe('wrapWithWixServices', () => {
|
|
19
|
+
it('resolves a registered service to its real implementation, stubbing nothing', () => {
|
|
20
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
21
|
+
|
|
22
|
+
const TranslatingComponent: ComponentType<unknown> = () => {
|
|
23
|
+
const translations = useService(TranslationsDefinition)
|
|
24
|
+
return React.createElement('span', null, translations.translate('app')('greeting') ?? 'fallback-text')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// The real service echoes the key back; a stub would have produced an empty string.
|
|
28
|
+
expect(renderThroughWrapper(TranslatingComponent)).toContain('<span>greeting</span>')
|
|
29
|
+
expect(warn).not.toHaveBeenCalled()
|
|
30
|
+
|
|
31
|
+
warn.mockRestore()
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
it('warns per extraction', () => {
|
|
35
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
36
|
+
|
|
37
|
+
const AnalyticsComponent: ComponentType<unknown> = () => {
|
|
38
|
+
useService(UnregisteredDefinition)
|
|
39
|
+
return null
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// A module-level dedupe would leave every component after the first silently degraded.
|
|
43
|
+
renderThroughWrapper(AnalyticsComponent)
|
|
44
|
+
renderThroughWrapper(AnalyticsComponent)
|
|
45
|
+
|
|
46
|
+
expect(warn).toHaveBeenCalledTimes(2)
|
|
47
|
+
|
|
48
|
+
warn.mockRestore()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('renders a component that reads an unregistered service instead of throwing', () => {
|
|
52
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
|
53
|
+
|
|
54
|
+
const AnalyticsComponent: ComponentType<unknown> = () => {
|
|
55
|
+
const analytics = useService(UnregisteredDefinition)
|
|
56
|
+
return React.createElement('div', { 'data-tracking-id': String(analytics.trackingId) }, 'rendered')
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
expect(renderThroughWrapper(AnalyticsComponent)).toContain('<div data-tracking-id="">rendered</div>')
|
|
60
|
+
|
|
61
|
+
warn.mockRestore()
|
|
62
|
+
})
|
|
63
|
+
})
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
import { SeoServiceDefinition } from '@wix/seo-service/definition'
|
|
2
|
+
import { createSeoService } from '@wix/seo-service/implementations'
|
|
3
|
+
import { createServicesManager, createServicesMap } from '@wix/services-manager'
|
|
4
|
+
import { ServicesManagerProvider } from '@wix/services-manager-react'
|
|
5
|
+
import {
|
|
6
|
+
ProvideComponentServiceDefinition,
|
|
7
|
+
ProvideComponentServiceFactory,
|
|
8
|
+
} from '@wix/services-manager-react/core-services/provide-component'
|
|
9
|
+
import type { ServiceBinding } from '@wix/services-manager/types'
|
|
10
|
+
import { ConfigurationDefinition } from '@wix/site-service-configuration/definition'
|
|
11
|
+
import { ConfigurationService } from '@wix/site-service-configuration/implementations'
|
|
12
|
+
import { DeviceInfoDefinition } from '@wix/site-service-device-info/definition'
|
|
13
|
+
import { DeviceInfoService } from '@wix/site-service-device-info/implementations'
|
|
14
|
+
import { EditorContextDefinition } from '@wix/site-service-editor-context/definition'
|
|
15
|
+
import { EditorContextService } from '@wix/site-service-editor-context/implementations'
|
|
16
|
+
import { ExperimentsDefinition } from '@wix/site-service-experiments/definition'
|
|
17
|
+
import { ExperimentsService } from '@wix/site-service-experiments/implementations'
|
|
18
|
+
import { LocaleDefinition } from '@wix/site-service-locale/definition'
|
|
19
|
+
import { LocaleService } from '@wix/site-service-locale/implementations'
|
|
20
|
+
import type { IPagesServiceConfig, Page } from '@wix/site-service-pages/definition'
|
|
21
|
+
import { PagesDefinition } from '@wix/site-service-pages/definition'
|
|
22
|
+
import { PagesService } from '@wix/site-service-pages/implementations'
|
|
23
|
+
import { ProvideCssDefinition as SiteProvideCssDefinition } from '@wix/site-service-provide-css/definition'
|
|
24
|
+
import { ProvideCssService as SiteProvideCssService } from '@wix/site-service-provide-css/implementations'
|
|
25
|
+
import { RenderingContextDefinition } from '@wix/site-service-rendering-context/definition'
|
|
26
|
+
import { RenderingContextService } from '@wix/site-service-rendering-context/implementations'
|
|
27
|
+
import { UrlDefinition as SiteUrlDefinition } from '@wix/site-service-url/definition'
|
|
28
|
+
import { UrlService as SiteUrlService } from '@wix/site-service-url/implementations'
|
|
29
|
+
import { AnchorsDefinition } from '@wix/viewer-service-anchors/definition'
|
|
30
|
+
import { AnchorsService } from '@wix/viewer-service-anchors/implementations'
|
|
31
|
+
import { ConsentPolicyDefinition } from '@wix/viewer-service-consent-policy/definition'
|
|
32
|
+
import { ConsentPolicyService } from '@wix/viewer-service-consent-policy/implementations'
|
|
33
|
+
import { EnvironmentDefinition } from '@wix/viewer-service-environment/definition'
|
|
34
|
+
import { EnvironmentService } from '@wix/viewer-service-environment/implementations'
|
|
35
|
+
import { LinkUtilsDefinition } from '@wix/viewer-service-link-utils/definition'
|
|
36
|
+
import { LinkUtilsService } from '@wix/viewer-service-link-utils/implementations'
|
|
37
|
+
import { NamedSignalsDefinition } from '@wix/viewer-service-named-signals/definition'
|
|
38
|
+
import { NamedSignalsService } from '@wix/viewer-service-named-signals/implementations'
|
|
39
|
+
import type { IPagesServiceConfig as IViewerPagesServiceConfig } from '@wix/viewer-service-pages/definition'
|
|
40
|
+
import { PagesDefinition as ViewerPagesDefinition } from '@wix/viewer-service-pages/definition'
|
|
41
|
+
import { PagesService as ViewerPagesService } from '@wix/viewer-service-pages/implementations'
|
|
42
|
+
import { ProvideCssDefinition } from '@wix/viewer-service-provide-css/definition'
|
|
43
|
+
import { ProvideCssService } from '@wix/viewer-service-provide-css/implementations'
|
|
44
|
+
import { RendererConfigurationDefinition } from '@wix/viewer-service-renderer-configuration/definition'
|
|
45
|
+
import { RendererConfigurationService } from '@wix/viewer-service-renderer-configuration/implementations'
|
|
46
|
+
import { SdkStateDefinition } from '@wix/viewer-service-sdk-state/definition'
|
|
47
|
+
import { SdkStateService } from '@wix/viewer-service-sdk-state/implementations'
|
|
48
|
+
import { SiteScrollBlockerDefinition } from '@wix/viewer-service-site-scroll-blocker/definition'
|
|
49
|
+
import { SiteScrollBlockerService } from '@wix/viewer-service-site-scroll-blocker/implementations'
|
|
50
|
+
import type { Topology } from '@wix/viewer-service-topology/definition'
|
|
51
|
+
import { TopologyDefinition } from '@wix/viewer-service-topology/definition'
|
|
52
|
+
import { TopologyService } from '@wix/viewer-service-topology/implementations'
|
|
53
|
+
import { TranslationsDefinition } from '@wix/viewer-service-translations/definition'
|
|
54
|
+
import { TranslationsService } from '@wix/viewer-service-translations/implementations'
|
|
55
|
+
import { UrlDefinition } from '@wix/viewer-service-url/definition'
|
|
56
|
+
import { UrlService } from '@wix/viewer-service-url/implementations'
|
|
57
|
+
import React, { type ComponentType, type ReactNode } from 'react'
|
|
58
|
+
|
|
59
|
+
import { createAutoStubFallbackManager } from './service-stub'
|
|
60
|
+
|
|
61
|
+
const PLACEHOLDER_SITE_URL = 'https://wix.com'
|
|
62
|
+
const PLACEHOLDER_EXPERIMENTS = {
|
|
63
|
+
'specs.thunderbolt.isClassNameToRootEnabled': true,
|
|
64
|
+
}
|
|
65
|
+
const PLACEHOLDER_HOME_PAGE: Page = {
|
|
66
|
+
id: 'home',
|
|
67
|
+
title: 'Home',
|
|
68
|
+
path: '/',
|
|
69
|
+
popup: false,
|
|
70
|
+
}
|
|
71
|
+
const PLACEHOLDER_CURRENT_PAGE: Page = {
|
|
72
|
+
id: 'audio',
|
|
73
|
+
title: 'Audio',
|
|
74
|
+
path: '/shop/audio',
|
|
75
|
+
parentPageId: 'shop',
|
|
76
|
+
popup: false,
|
|
77
|
+
}
|
|
78
|
+
const PLACEHOLDER_PAGES_CONFIG: IPagesServiceConfig = {
|
|
79
|
+
pages: {
|
|
80
|
+
home: { title: 'Home', path: '/', popup: false },
|
|
81
|
+
shop: { title: 'Shop', path: '/shop', parentPageId: 'home', popup: false },
|
|
82
|
+
audio: { title: 'Audio', path: '/shop/audio', parentPageId: 'shop', popup: false },
|
|
83
|
+
},
|
|
84
|
+
mainPage: PLACEHOLDER_HOME_PAGE,
|
|
85
|
+
currentPage: PLACEHOLDER_CURRENT_PAGE,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Derived, so the deprecated pages service and its successor describe the same site. */
|
|
89
|
+
const PLACEHOLDER_VIEWER_PAGES_CONFIG: IViewerPagesServiceConfig = {
|
|
90
|
+
pages: Object.fromEntries(
|
|
91
|
+
Object.entries(PLACEHOLDER_PAGES_CONFIG.pages).map(([pageId, { title, path, parentPageId }]) => [
|
|
92
|
+
pageId,
|
|
93
|
+
{ title, path, parentPageId },
|
|
94
|
+
]),
|
|
95
|
+
),
|
|
96
|
+
currentPageId: PLACEHOLDER_PAGES_CONFIG.currentPage.id,
|
|
97
|
+
mainPageId: PLACEHOLDER_PAGES_CONFIG.mainPage.id,
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const PLACEHOLDER_TOPOLOGY: Topology = {
|
|
101
|
+
mediaRootUrl: 'https://static.wixstatic.com/',
|
|
102
|
+
staticMediaUrl: 'https://static.wixstatic.com/media',
|
|
103
|
+
fileRepoUrl: 'https://static.wixstatic.com/ugd',
|
|
104
|
+
staticHTMLComponentUrl: 'https://static.wixstatic.com/html',
|
|
105
|
+
scriptsUrl: 'https://static.parastorage.com/',
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// biome-ignore lint/suspicious/noExplicitAny: service bindings are heterogeneous by construction
|
|
109
|
+
type AnyServiceBinding = ServiceBinding<any, any>
|
|
110
|
+
|
|
111
|
+
/** Anything omitted here resolves through {@link createAutoStubFallbackManager} instead. */
|
|
112
|
+
function createServiceBindings(): AnyServiceBinding[] {
|
|
113
|
+
return [
|
|
114
|
+
{
|
|
115
|
+
definition: EnvironmentDefinition,
|
|
116
|
+
impl: EnvironmentService,
|
|
117
|
+
config: { experiments: PLACEHOLDER_EXPERIMENTS },
|
|
118
|
+
},
|
|
119
|
+
{ definition: TranslationsDefinition, impl: TranslationsService, config: { translations: {} } },
|
|
120
|
+
{
|
|
121
|
+
definition: UrlDefinition,
|
|
122
|
+
impl: UrlService,
|
|
123
|
+
config: { externalBaseUrl: PLACEHOLDER_SITE_URL, requestUrl: PLACEHOLDER_SITE_URL },
|
|
124
|
+
},
|
|
125
|
+
{ definition: LinkUtilsDefinition, impl: LinkUtilsService, config: {} },
|
|
126
|
+
{ definition: AnchorsDefinition, impl: AnchorsService, config: {} },
|
|
127
|
+
{ definition: SiteScrollBlockerDefinition, impl: SiteScrollBlockerService, config: {} },
|
|
128
|
+
{ definition: ProvideCssDefinition, impl: ProvideCssService, config: {} },
|
|
129
|
+
{ definition: RenderingContextDefinition, impl: RenderingContextService, config: {} },
|
|
130
|
+
{ definition: ExperimentsDefinition, impl: ExperimentsService, config: { experiments: PLACEHOLDER_EXPERIMENTS } },
|
|
131
|
+
{
|
|
132
|
+
definition: ConfigurationDefinition,
|
|
133
|
+
impl: ConfigurationService,
|
|
134
|
+
config: { qaMode: false, trackClicksAnalytics: false, sandboxInHTMLComp: false },
|
|
135
|
+
},
|
|
136
|
+
{ definition: SiteProvideCssDefinition, impl: SiteProvideCssService, config: {} },
|
|
137
|
+
{ definition: DeviceInfoDefinition, impl: DeviceInfoService, config: {} },
|
|
138
|
+
{ definition: EditorContextDefinition, impl: EditorContextService, config: { previewMode: false } },
|
|
139
|
+
{
|
|
140
|
+
definition: SiteUrlDefinition,
|
|
141
|
+
impl: SiteUrlService,
|
|
142
|
+
config: {
|
|
143
|
+
currentUrl: PLACEHOLDER_SITE_URL,
|
|
144
|
+
siteUrl: PLACEHOLDER_SITE_URL,
|
|
145
|
+
pages: [],
|
|
146
|
+
pageIdToPrefix: {},
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{ definition: PagesDefinition, impl: PagesService, config: PLACEHOLDER_PAGES_CONFIG },
|
|
150
|
+
{ definition: ViewerPagesDefinition, impl: ViewerPagesService, config: PLACEHOLDER_VIEWER_PAGES_CONFIG },
|
|
151
|
+
{ definition: LocaleDefinition, impl: LocaleService, config: { language: 'en', direction: 'ltr' } },
|
|
152
|
+
{ definition: TopologyDefinition, impl: TopologyService, config: { topology: PLACEHOLDER_TOPOLOGY } },
|
|
153
|
+
{
|
|
154
|
+
definition: RendererConfigurationDefinition,
|
|
155
|
+
impl: RendererConfigurationService,
|
|
156
|
+
config: { isHipaaCompliant: false, currency: 'USD' },
|
|
157
|
+
},
|
|
158
|
+
{ definition: SdkStateDefinition, impl: SdkStateService, config: {} },
|
|
159
|
+
{ definition: NamedSignalsDefinition, impl: NamedSignalsService, config: {} },
|
|
160
|
+
{ definition: ConsentPolicyDefinition, impl: ConsentPolicyService, config: { siteConsentPolicy: undefined } },
|
|
161
|
+
{
|
|
162
|
+
definition: SeoServiceDefinition,
|
|
163
|
+
impl: createSeoService,
|
|
164
|
+
config: { seoApi: { setVeloSeoTags: async () => {}, resetVeloSeoTags: async () => {} } },
|
|
165
|
+
},
|
|
166
|
+
{ definition: ProvideComponentServiceDefinition, impl: ProvideComponentServiceFactory, config: {} },
|
|
167
|
+
]
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Dedupes per wrapper instance, so every extraction reports its own stubs. */
|
|
171
|
+
function createStubbedServiceReporter(): (serviceId: string) => void {
|
|
172
|
+
const reportedServiceIds = new Set<string>()
|
|
173
|
+
|
|
174
|
+
return (serviceId) => {
|
|
175
|
+
if (reportedServiceIds.has(serviceId)) {
|
|
176
|
+
return
|
|
177
|
+
}
|
|
178
|
+
reportedServiceIds.add(serviceId)
|
|
179
|
+
console.warn(
|
|
180
|
+
`[zero-config] Service "${serviceId}" is not registered by the extraction wrapper and was replaced with an inert stub. Branches depending on it may be missing from the extracted manifest.`,
|
|
181
|
+
)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Local replacement for `@wix/builder-services-wrapper`'s `WixServicesWrapper`. */
|
|
186
|
+
export function WixServicesWrapper({ children }: { children?: ReactNode }): React.ReactElement {
|
|
187
|
+
// Memoized: the manager eagerly constructs every registered service on creation.
|
|
188
|
+
const servicesManager = React.useMemo(
|
|
189
|
+
() =>
|
|
190
|
+
createServicesManager(
|
|
191
|
+
createServicesMap(createServiceBindings()),
|
|
192
|
+
createAutoStubFallbackManager({ onServiceStubbed: createStubbedServiceReporter() }),
|
|
193
|
+
),
|
|
194
|
+
[],
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
return React.createElement(ServicesManagerProvider, { servicesManager }, children)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function wrapWithWixServices(Component: ComponentType<unknown>): ComponentType<unknown> {
|
|
201
|
+
const WrappedComponent: ComponentType<unknown> = (props) =>
|
|
202
|
+
React.createElement(WixServicesWrapper, null, React.createElement(Component, props as Record<string, unknown>))
|
|
203
|
+
return WrappedComponent
|
|
204
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { WixServicesWrapper } from '@wix/builder-services-wrapper'
|
|
2
1
|
import type { EditorReactComponent } from '@wix/react-component-schema'
|
|
3
2
|
import { Result, ResultAsync } from 'neverthrow'
|
|
4
3
|
import React, { type ComponentType } from 'react'
|
|
@@ -9,6 +8,7 @@ import { buildContextProviderModules } from './extensions/context-providers/cont
|
|
|
9
8
|
import { buildContextAwareWrapper, loadMockProviders } from './extensions/context-providers/mock-provider'
|
|
10
9
|
import { buildRefElementContext } from './extensions/ref-elements/context'
|
|
11
10
|
import type { RefElementContext } from './extensions/ref-elements/types'
|
|
11
|
+
import { wrapWithWixServices } from './extensions/wix-services/wrapper'
|
|
12
12
|
import type { ExtractionError } from './extraction-types'
|
|
13
13
|
import type { RunExtractorsOptions } from './information-extractors/react'
|
|
14
14
|
import { extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
|
|
@@ -17,11 +17,7 @@ import { processComponent } from './manifest-pipeline'
|
|
|
17
17
|
import { findComponent, findDefaultComponent, loadModuleForExtraction } from './module-loader'
|
|
18
18
|
import { compileTsFile } from './ts-compiler'
|
|
19
19
|
|
|
20
|
-
const defaultWrapper =
|
|
21
|
-
const WrappedComponent: ComponentType<unknown> = (props) =>
|
|
22
|
-
React.createElement(WixServicesWrapper, null, React.createElement(Component, props as Record<string, unknown>))
|
|
23
|
-
return WrappedComponent
|
|
24
|
-
}
|
|
20
|
+
const defaultWrapper = wrapWithWixServices
|
|
25
21
|
|
|
26
22
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
27
23
|
// Types
|
|
@@ -18,6 +18,11 @@ const inheritedChildrenFixture = path.resolve(
|
|
|
18
18
|
'../../../../example-components/src/components/InheritedChildren/component.tsx',
|
|
19
19
|
)
|
|
20
20
|
|
|
21
|
+
const activeItemIndexFixture = path.resolve(
|
|
22
|
+
__dirname,
|
|
23
|
+
'../../../../example-components/src/components/ActiveItemIndex/SimpleTabs/component.tsx',
|
|
24
|
+
)
|
|
25
|
+
|
|
21
26
|
describe('TS extractor — ElementState<> trigger detection', () => {
|
|
22
27
|
it('flags an ElementState<>-wrapped prop and unwraps its inner type', async () => {
|
|
23
28
|
const programResult = await compileTsFile(propTriggeredFixture)
|
|
@@ -79,3 +84,35 @@ describe('TS extractor — container children props', () => {
|
|
|
79
84
|
expect(component!.props.label).toBeDefined()
|
|
80
85
|
})
|
|
81
86
|
})
|
|
87
|
+
|
|
88
|
+
describe('TS extractor — ActiveItemIndex<> detection', () => {
|
|
89
|
+
it('flags an ActiveItemIndex<>-wrapped prop with the target array prop name and unwraps to number', async () => {
|
|
90
|
+
const programResult = await compileTsFile(activeItemIndexFixture)
|
|
91
|
+
expect(programResult.isOk()).toBe(true)
|
|
92
|
+
const program = programResult._unsafeUnwrap()
|
|
93
|
+
|
|
94
|
+
const components = extractAllComponentInfo(program, activeItemIndexFixture)
|
|
95
|
+
const component = components.find((candidate) => candidate.componentName === 'SimpleTabs')
|
|
96
|
+
expect(component).toBeDefined()
|
|
97
|
+
|
|
98
|
+
const activeTab = component!.props.activeTab
|
|
99
|
+
expect(activeTab.activeItemIndexTarget).toBe('tabs')
|
|
100
|
+
// The `ActiveItemIndex<'tabs'>` wrapper is unwrapped — the prop behaves as a plain number.
|
|
101
|
+
expect(activeTab.type).toBe('number')
|
|
102
|
+
// It should not be flagged as a state trigger.
|
|
103
|
+
expect(activeTab.isStateTrigger).toBeUndefined()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('does not flag a plain number prop as an active item index', async () => {
|
|
107
|
+
const programResult = await compileTsFile(activeItemIndexFixture)
|
|
108
|
+
expect(programResult.isOk()).toBe(true)
|
|
109
|
+
const program = programResult._unsafeUnwrap()
|
|
110
|
+
|
|
111
|
+
const components = extractAllComponentInfo(program, activeItemIndexFixture)
|
|
112
|
+
const component = components.find((candidate) => candidate.componentName === 'SimpleTabs')
|
|
113
|
+
expect(component).toBeDefined()
|
|
114
|
+
|
|
115
|
+
// `tabs` is an array prop, not an active item index.
|
|
116
|
+
expect(component!.props.tabs.activeItemIndexTarget).toBeUndefined()
|
|
117
|
+
})
|
|
118
|
+
})
|
|
@@ -211,25 +211,32 @@ function convertComponentDoc(doc: ComponentDoc, program: ts.Program, checker: ts
|
|
|
211
211
|
// Resolve the type deeply
|
|
212
212
|
let declaredTypeInfo: DeclaredTypeInfo | undefined
|
|
213
213
|
let stateTrigger: StateTriggerInfo | undefined
|
|
214
|
+
let activeItemIndex: ActiveItemIndexInfo | undefined
|
|
214
215
|
if (propSymbol) {
|
|
215
216
|
const decl = propSymbol.getDeclarations()?.[0]
|
|
216
217
|
if (decl) {
|
|
217
218
|
const declSourceFile = decl.getSourceFile()
|
|
218
219
|
declaredTypeInfo = getDeclaredTypeInfo(propSymbol, declSourceFile, checker)
|
|
219
220
|
stateTrigger = detectStateTrigger(propSymbol, declSourceFile, checker)
|
|
221
|
+
activeItemIndex = detectActiveItemIndex(propSymbol, declSourceFile, checker)
|
|
220
222
|
}
|
|
221
223
|
}
|
|
222
224
|
|
|
223
225
|
// An `ElementState<Inner>` prop should behave as its inner type everywhere except
|
|
224
226
|
// for the `isStateTrigger` flag, so unwrap the inner type for `type` and resolution
|
|
225
227
|
// (and drop the `ElementState` declared symbol so it isn't mistaken for a semantic type).
|
|
226
|
-
|
|
228
|
+
// Similarly, `ActiveItemIndex<'arrayPropName'>` unwraps to `number`.
|
|
229
|
+
const typeString =
|
|
230
|
+
stateTrigger?.innerTypeName ??
|
|
231
|
+
(activeItemIndex ? 'number' : undefined) ??
|
|
232
|
+
declaredTypeInfo?.name ??
|
|
233
|
+
propItem.type.name
|
|
227
234
|
const resolvedType = propType
|
|
228
235
|
? resolveType({
|
|
229
236
|
type: propType,
|
|
230
237
|
checker,
|
|
231
238
|
typeString,
|
|
232
|
-
declaredSymbol: stateTrigger ? undefined : declaredTypeInfo?.symbol,
|
|
239
|
+
declaredSymbol: stateTrigger || activeItemIndex ? undefined : declaredTypeInfo?.symbol,
|
|
233
240
|
})
|
|
234
241
|
: { kind: 'primitive' as const, value: propItem.type.name }
|
|
235
242
|
|
|
@@ -255,6 +262,7 @@ function convertComponentDoc(doc: ComponentDoc, program: ts.Program, checker: ts
|
|
|
255
262
|
min: parseNumericTag(tags?.min, propName, 'min'),
|
|
256
263
|
max: parseNumericTag(tags?.max, propName, 'max'),
|
|
257
264
|
isStateTrigger: stateTrigger ? true : undefined,
|
|
265
|
+
activeItemIndexTarget: activeItemIndex?.targetArrayProp,
|
|
258
266
|
}
|
|
259
267
|
}
|
|
260
268
|
|
|
@@ -506,6 +514,11 @@ interface StateTriggerInfo {
|
|
|
506
514
|
innerTypeName: string
|
|
507
515
|
}
|
|
508
516
|
|
|
517
|
+
interface ActiveItemIndexInfo {
|
|
518
|
+
/** The string literal from the type argument, naming the target array prop. */
|
|
519
|
+
targetArrayProp: string
|
|
520
|
+
}
|
|
521
|
+
|
|
509
522
|
/**
|
|
510
523
|
* Detects when a prop is declared with the `ElementState<>` marker type from
|
|
511
524
|
* `@wix/react-component-utils` (e.g. `isLoading?: ElementState<boolean>`), which opts
|
|
@@ -558,6 +571,65 @@ function isElementStateAliasFromUtils(identifier: ts.Identifier, checker: ts.Typ
|
|
|
558
571
|
return declaration.getSourceFile().fileName.includes('react-component-utils')
|
|
559
572
|
}
|
|
560
573
|
|
|
574
|
+
/**
|
|
575
|
+
* Detects when a prop is declared with the `ActiveItemIndex<>` marker type from
|
|
576
|
+
* `@wix/react-component-utils` (e.g. `activeTab?: ActiveItemIndex<'tabs'>`), which
|
|
577
|
+
* links the prop to an array data prop for the `displayGroups.arrayItems` manifest
|
|
578
|
+
* block. Returns the target array prop name from the string literal type argument.
|
|
579
|
+
* Returns `undefined` for any other declaration, or when the type argument is missing
|
|
580
|
+
* or not a string literal.
|
|
581
|
+
*/
|
|
582
|
+
function detectActiveItemIndex(
|
|
583
|
+
prop: ts.Symbol,
|
|
584
|
+
sourceFile: ts.SourceFile,
|
|
585
|
+
checker: ts.TypeChecker,
|
|
586
|
+
): ActiveItemIndexInfo | undefined {
|
|
587
|
+
const declaration = prop.getDeclarations()?.[0]
|
|
588
|
+
if (!declaration) return undefined
|
|
589
|
+
|
|
590
|
+
if (
|
|
591
|
+
!(ts.isPropertySignature(declaration) || ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration)) ||
|
|
592
|
+
!declaration.type ||
|
|
593
|
+
!ts.isTypeReferenceNode(declaration.type)
|
|
594
|
+
) {
|
|
595
|
+
return undefined
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
const typeName = declaration.type.typeName
|
|
599
|
+
const identifier = ts.isIdentifier(typeName) ? typeName : typeName.right
|
|
600
|
+
if (identifier.text !== 'ActiveItemIndex') return undefined
|
|
601
|
+
if (!isActiveItemIndexAliasFromUtils(identifier, checker)) return undefined
|
|
602
|
+
|
|
603
|
+
// Require an explicit string literal type argument — bare ActiveItemIndex (no arg)
|
|
604
|
+
// compiles but gives ZeroConfig no array name to link to.
|
|
605
|
+
const typeArgs = declaration.type.typeArguments
|
|
606
|
+
if (!typeArgs || typeArgs.length !== 1) return undefined
|
|
607
|
+
|
|
608
|
+
const typeArgNode = typeArgs[0]
|
|
609
|
+
if (!ts.isLiteralTypeNode(typeArgNode) || !ts.isStringLiteral(typeArgNode.literal)) return undefined
|
|
610
|
+
|
|
611
|
+
return { targetArrayProp: typeArgNode.literal.text }
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Confirms an identifier referencing `ActiveItemIndex` resolves to the marker type
|
|
616
|
+
* alias exported by `@wix/react-component-utils`, rather than an unrelated local
|
|
617
|
+
* type that happens to be named `ActiveItemIndex`.
|
|
618
|
+
*/
|
|
619
|
+
function isActiveItemIndexAliasFromUtils(identifier: ts.Identifier, checker: ts.TypeChecker): boolean {
|
|
620
|
+
let symbol = checker.getSymbolAtLocation(identifier)
|
|
621
|
+
if (!symbol) return false
|
|
622
|
+
if ((symbol.getFlags() & ts.SymbolFlags.Alias) !== 0) {
|
|
623
|
+
symbol = checker.getAliasedSymbol(symbol)
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const declaration = symbol.getDeclarations()?.[0]
|
|
627
|
+
if (!declaration || !ts.isTypeAliasDeclaration(declaration)) return false
|
|
628
|
+
if (declaration.name.text !== 'ActiveItemIndex') return false
|
|
629
|
+
|
|
630
|
+
return declaration.getSourceFile().fileName.includes('react-component-utils')
|
|
631
|
+
}
|
|
632
|
+
|
|
561
633
|
function getDeclaredTypeInfo(
|
|
562
634
|
prop: ts.Symbol,
|
|
563
635
|
sourceFile: ts.SourceFile,
|
|
@@ -59,6 +59,11 @@ export interface PropInfo {
|
|
|
59
59
|
// state. `type` and `resolvedType` reflect the unwrapped inner type, so the prop
|
|
60
60
|
// still behaves as a normal data prop everywhere else.
|
|
61
61
|
isStateTrigger?: boolean
|
|
62
|
+
// When the prop is wrapped in `ActiveItemIndex<'arrayPropName'>` from
|
|
63
|
+
// @wix/react-component-utils, carries the target array prop name. `type` and
|
|
64
|
+
// `resolvedType` reflect the unwrapped `number` type. Used by the display-groups
|
|
65
|
+
// builder to auto-generate the `displayGroups.arrayItems` manifest block.
|
|
66
|
+
activeItemIndexTarget?: string
|
|
62
67
|
}
|
|
63
68
|
|
|
64
69
|
export interface ComponentInfo {
|