@wix/zero-config-implementation 1.107.0 → 1.108.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 +11 -0
- package/dist/index.js +11914 -11755
- package/package.json +2 -2
- package/src/converters/data-item-builder.ts +44 -10
- package/src/converters/function-signature-builder.test.ts +257 -0
- package/src/converters/function-signature-builder.ts +144 -0
- package/src/information-extractors/ts/types.ts +6 -0
- package/src/information-extractors/ts/utils/semantic-type-resolver.ts +93 -2
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"registry": "https://registry.npmjs.org/",
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
7
|
+
"version": "1.108.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",
|
|
@@ -106,5 +106,5 @@
|
|
|
106
106
|
]
|
|
107
107
|
}
|
|
108
108
|
},
|
|
109
|
-
"falconPackageHash": "
|
|
109
|
+
"falconPackageHash": "7adb0b7422620a1dd6289f8b7dd6936c7e173aeb90665b60b9a731f1"
|
|
110
110
|
}
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* intentional because new type kinds are expected as the TypeScript type system
|
|
9
9
|
* evolves, and a text fallback is always a safe default.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
* resolved
|
|
11
|
+
* Function signatures fail explicitly when their types cannot be represented.
|
|
12
|
+
* Arrays also fail when resolved metadata is inconsistent (no elementType).
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import type { DataItem, DomEvent } from '@wix/react-component-schema'
|
|
@@ -21,6 +21,7 @@ import type { DOMBinding, TrackingStores } from '../information-extractors/react
|
|
|
21
21
|
import type { PropInfo, ResolvedType } from '../information-extractors/ts/types'
|
|
22
22
|
import { WIX_TYPE_TO_DATA_TYPE } from '../wix-type-to-data-type'
|
|
23
23
|
import { collectUsedA11yAttributes } from './a11y-builder'
|
|
24
|
+
import { buildFunctionSignature } from './function-signature-builder'
|
|
24
25
|
import { formatDisplayName } from './utils'
|
|
25
26
|
|
|
26
27
|
const { DATA_TYPE } = DATA
|
|
@@ -57,8 +58,8 @@ function getBindingsForPath(propUsages: TrackingStores['propUsages'], path: stri
|
|
|
57
58
|
* @param defaultValue - Optional default value for the data item.
|
|
58
59
|
* @param propUsages - The full stores.propUsages map for on-demand binding lookups.
|
|
59
60
|
* @param propPath - The full stores.propUsages key for this prop, e.g. "props.linkUrl".
|
|
60
|
-
* @returns `Ok<DataItem>` on success, `Err<ParseError>`
|
|
61
|
-
*
|
|
61
|
+
* @returns `Ok<DataItem>` on success, `Err<ParseError>` for unsupported function
|
|
62
|
+
* signatures or inconsistent resolved metadata.
|
|
62
63
|
*/
|
|
63
64
|
export function buildDataItem(
|
|
64
65
|
propInfo: PropInfo,
|
|
@@ -120,8 +121,16 @@ function applyResolvedTypeToDataItem(
|
|
|
120
121
|
return handleUnionType({ dataItem, resolvedType, propInfo, propUsages, propPath })
|
|
121
122
|
|
|
122
123
|
case 'function':
|
|
123
|
-
|
|
124
|
-
|
|
124
|
+
{
|
|
125
|
+
const signatureResult = buildFunctionSignature(resolvedType, propInfo.name)
|
|
126
|
+
if (signatureResult.isErr()) return err(signatureResult.error)
|
|
127
|
+
if (signatureResult.value) {
|
|
128
|
+
dataItem.dataType = DATA_TYPE.function
|
|
129
|
+
dataItem.function = signatureResult.value
|
|
130
|
+
return ok(undefined)
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return handleFunctionType(dataItem, propInfo, resolvedType, bindings)
|
|
125
134
|
|
|
126
135
|
case 'semantic':
|
|
127
136
|
handleSemanticType({ dataItem, resolvedType, propUsages, propPath })
|
|
@@ -309,6 +318,17 @@ function handleUnionType({
|
|
|
309
318
|
return ok(undefined)
|
|
310
319
|
}
|
|
311
320
|
|
|
321
|
+
if (resolvedType.types.some((member) => member.kind === 'function')) {
|
|
322
|
+
const definedTypes = resolvedType.types.filter(
|
|
323
|
+
(member) => !(member.kind === 'primitive' && member.value === 'undefined'),
|
|
324
|
+
)
|
|
325
|
+
if (definedTypes.length !== 1) {
|
|
326
|
+
return err(
|
|
327
|
+
new ParseError(`Cannot represent callback union for ${propInfo.name}; use a single supported signature.`),
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
312
332
|
// Filter out undefined and null types
|
|
313
333
|
const validTypes = resolvedType.types.filter((t) => t.value !== 'undefined' && t.value !== 'null')
|
|
314
334
|
|
|
@@ -423,8 +443,8 @@ const EVENT_ATTR_TO_DOM_EVENT: Record<string, DomEvent> = {
|
|
|
423
443
|
}
|
|
424
444
|
|
|
425
445
|
/**
|
|
426
|
-
*
|
|
427
|
-
*
|
|
446
|
+
* Legacy fallback when signature information is missing or uninformative.
|
|
447
|
+
* Maps recognized DOM handlers to event parameters; unresolved custom signatures fail.
|
|
428
448
|
*
|
|
429
449
|
* Checks bound DOM attributes first (reflects actual DOM wiring regardless of prop name),
|
|
430
450
|
* then falls back to prop name matching.
|
|
@@ -432,7 +452,12 @@ const EVENT_ATTR_TO_DOM_EVENT: Record<string, DomEvent> = {
|
|
|
432
452
|
* The `domEvent` field exists in the proto schema but is not yet present in the installed
|
|
433
453
|
* FunctionParameter typings — cast through `unknown` to bridge the version gap.
|
|
434
454
|
*/
|
|
435
|
-
function handleFunctionType(
|
|
455
|
+
function handleFunctionType(
|
|
456
|
+
dataItem: DataItem,
|
|
457
|
+
propInfo: PropInfo,
|
|
458
|
+
resolvedType: ResolvedType,
|
|
459
|
+
bindings?: DOMBinding[],
|
|
460
|
+
): Result<void, ParseErrorInstance> {
|
|
436
461
|
let domEvent: DomEvent | undefined
|
|
437
462
|
|
|
438
463
|
if (bindings) {
|
|
@@ -452,10 +477,19 @@ function handleFunctionType(dataItem: DataItem, propInfo: PropInfo, bindings?: D
|
|
|
452
477
|
dataItem.dataType = DATA_TYPE.function
|
|
453
478
|
|
|
454
479
|
if (domEvent) {
|
|
455
|
-
|
|
480
|
+
const optional = resolvedType.functionSignature?.parameters[0]?.optional
|
|
481
|
+
dataItem.function = {
|
|
482
|
+
parameters: [{ domEvent, ...(optional ? { optional: true } : {}) }],
|
|
483
|
+
} as unknown as DataItem['function']
|
|
456
484
|
} else {
|
|
485
|
+
if (resolvedType.functionSignature) {
|
|
486
|
+
return err(
|
|
487
|
+
new ParseError(`Cannot infer function parameter for ${propInfo.name}; declare a supported explicit type.`),
|
|
488
|
+
)
|
|
489
|
+
}
|
|
457
490
|
dataItem.function = {}
|
|
458
491
|
}
|
|
492
|
+
return ok(undefined)
|
|
459
493
|
}
|
|
460
494
|
|
|
461
495
|
/**
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import path from 'node:path'
|
|
2
|
+
import ts from 'typescript'
|
|
3
|
+
import { describe, expect, it } from 'vitest'
|
|
4
|
+
import type { TrackingStores } from '../information-extractors/react'
|
|
5
|
+
import { extractAllComponentInfo } from '../information-extractors/ts/components'
|
|
6
|
+
import { resolveType } from '../information-extractors/ts/utils/semantic-type-resolver'
|
|
7
|
+
import { compileTsFile } from '../ts-compiler'
|
|
8
|
+
import { buildDataItem } from './data-item-builder'
|
|
9
|
+
|
|
10
|
+
const fixturePath = path.resolve(__dirname, '__signature_fixture__.ts')
|
|
11
|
+
const aliasPath = path.resolve(__dirname, '__signature_alias__.ts')
|
|
12
|
+
const fixtures = new Map([
|
|
13
|
+
[
|
|
14
|
+
fixturePath,
|
|
15
|
+
`
|
|
16
|
+
import type * as React from 'react'
|
|
17
|
+
import type { Email, Phone, WebUrl, Link, Image, Container } from '@wix/editor-react-types'
|
|
18
|
+
import type { ValueHandler, ChangeHandler, EmailHandler } from './__signature_alias__'
|
|
19
|
+
interface Callbacks {
|
|
20
|
+
onChange: (value: string) => void
|
|
21
|
+
onValueChange: ValueHandler
|
|
22
|
+
onOptionalValueChange?: ValueHandler
|
|
23
|
+
onUndefinedCallback: ValueHandler | undefined
|
|
24
|
+
onCallbackUnion: ((value: string) => void) | ((value: number) => void)
|
|
25
|
+
onNullableCallback: ValueHandler | null
|
|
26
|
+
onMixedCallback: ValueHandler | string
|
|
27
|
+
onOptionalNullableCallback?: ValueHandler | null
|
|
28
|
+
onOptionalBoolean: (value?: boolean) => void
|
|
29
|
+
onCheckedChange: (checked: boolean) => void
|
|
30
|
+
onNumberChange: (value: number) => void
|
|
31
|
+
onSelectionChange: (values: readonly string[]) => void
|
|
32
|
+
onDetails: (value: string, details?: { committed: boolean; count?: number }) => void
|
|
33
|
+
onTooMany: (one: string, two: string, three: string, four: string, five: string, six: string, seven: string, eight: string, nine: string, ten: string, eleven: string) => void
|
|
34
|
+
onClick: () => void
|
|
35
|
+
onEvent: ChangeHandler
|
|
36
|
+
onMouse: (event: React.MouseEvent) => void
|
|
37
|
+
onPointer: (event: React.PointerEvent) => void
|
|
38
|
+
onFocus: (event: React.FocusEvent) => void
|
|
39
|
+
onKeyboard: (event: React.KeyboardEvent) => void
|
|
40
|
+
onSubmit: (event: React.FormEvent) => void
|
|
41
|
+
onClickLegacy: (event: any) => void
|
|
42
|
+
calculate: (value: number) => Promise<number>
|
|
43
|
+
onLoad: () => Promise<void>
|
|
44
|
+
onUnsupported: (value: string | number) => void
|
|
45
|
+
onNullable: (value: string | null) => void
|
|
46
|
+
onRest: (...values: string[]) => void
|
|
47
|
+
onUnknown: (value: unknown) => void
|
|
48
|
+
onMode: (value: 'on' | 'off') => void
|
|
49
|
+
onEmail: (value: Email) => void
|
|
50
|
+
onEmailAlias: EmailHandler
|
|
51
|
+
getEmail: () => Email
|
|
52
|
+
loadEmail: () => Promise<Email>
|
|
53
|
+
onPhone: (value: Phone) => void
|
|
54
|
+
onUrl: (value: WebUrl) => void
|
|
55
|
+
onLink: (value: Link) => void
|
|
56
|
+
onImage: (value: Image) => void
|
|
57
|
+
onContainer: (value: Container) => void
|
|
58
|
+
overloaded: { (value: string): void; (value: number): void }
|
|
59
|
+
}
|
|
60
|
+
`,
|
|
61
|
+
],
|
|
62
|
+
[
|
|
63
|
+
aliasPath,
|
|
64
|
+
`
|
|
65
|
+
import type { ChangeEvent } from 'react'
|
|
66
|
+
import type { Email as EmailAddress } from '@wix/editor-react-types'
|
|
67
|
+
type ContactEmail = EmailAddress
|
|
68
|
+
export type EmailHandler = (value: ContactEmail) => void
|
|
69
|
+
export type Name = string
|
|
70
|
+
export type ValueHandler = (value: Name) => void
|
|
71
|
+
export type InputChange = ChangeEvent<HTMLInputElement>
|
|
72
|
+
export type ChangeHandler = (event: InputChange) => void
|
|
73
|
+
`,
|
|
74
|
+
],
|
|
75
|
+
])
|
|
76
|
+
const compilerOptions: ts.CompilerOptions = {
|
|
77
|
+
strict: true,
|
|
78
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
79
|
+
module: ts.ModuleKind.ESNext,
|
|
80
|
+
skipLibCheck: true,
|
|
81
|
+
noEmit: true,
|
|
82
|
+
}
|
|
83
|
+
const compilerHost = ts.createCompilerHost(compilerOptions)
|
|
84
|
+
const originalReadFile = compilerHost.readFile.bind(compilerHost)
|
|
85
|
+
const originalFileExists = compilerHost.fileExists.bind(compilerHost)
|
|
86
|
+
compilerHost.readFile = (fileName) => fixtures.get(fileName) ?? originalReadFile(fileName)
|
|
87
|
+
compilerHost.fileExists = (fileName) => fixtures.has(fileName) || originalFileExists(fileName)
|
|
88
|
+
compilerHost.getSourceFile = (fileName, languageVersion) => {
|
|
89
|
+
const source = compilerHost.readFile(fileName)
|
|
90
|
+
return source === undefined ? undefined : ts.createSourceFile(fileName, source, languageVersion, true)
|
|
91
|
+
}
|
|
92
|
+
const program = ts.createProgram([fixturePath, aliasPath], compilerOptions, compilerHost)
|
|
93
|
+
const checker = program.getTypeChecker()
|
|
94
|
+
const sourceFile = program.getSourceFile(fixturePath)!
|
|
95
|
+
const callbacksDeclaration = sourceFile.statements.find(ts.isInterfaceDeclaration)!
|
|
96
|
+
const callbacks = checker.getTypeAtLocation(callbacksDeclaration)
|
|
97
|
+
|
|
98
|
+
function buildCallback(name: string, manifestName = name, propUsages?: TrackingStores['propUsages']) {
|
|
99
|
+
const symbol = callbacks.getProperty(name)!
|
|
100
|
+
const type = checker.getTypeOfSymbolAtLocation(symbol, symbol.valueDeclaration!)
|
|
101
|
+
return buildDataItem(
|
|
102
|
+
{
|
|
103
|
+
name: manifestName,
|
|
104
|
+
type: checker.typeToString(type),
|
|
105
|
+
required: true,
|
|
106
|
+
resolvedType: resolveType({ type, checker }),
|
|
107
|
+
},
|
|
108
|
+
undefined,
|
|
109
|
+
propUsages,
|
|
110
|
+
`props.${manifestName}`,
|
|
111
|
+
)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
describe('function signatures from TypeScript to manifest', () => {
|
|
115
|
+
it.each([
|
|
116
|
+
['ForwardRefComponent', 'onChange'],
|
|
117
|
+
['ComplexProps', 'onUpdate'],
|
|
118
|
+
['PropBindingTypes', 'onSubmit'],
|
|
119
|
+
])('retains signatures through the actual component extractor for %s', async (componentName, callbackName) => {
|
|
120
|
+
const componentPath = path.resolve(
|
|
121
|
+
__dirname,
|
|
122
|
+
'../../../example-components/src/components',
|
|
123
|
+
componentName,
|
|
124
|
+
'component.tsx',
|
|
125
|
+
)
|
|
126
|
+
const program = (await compileTsFile(componentPath))._unsafeUnwrap()
|
|
127
|
+
const component = extractAllComponentInfo(program, componentPath).find(
|
|
128
|
+
(candidate) => candidate.componentName === componentName,
|
|
129
|
+
)!
|
|
130
|
+
const manifest = buildDataItem(component.props[callbackName])._unsafeUnwrap()
|
|
131
|
+
expect(manifest.dataType).toBe('function')
|
|
132
|
+
expect(manifest.function?.parameters?.length).toBeGreaterThan(0)
|
|
133
|
+
expect(JSON.parse(JSON.stringify(manifest)).function).toEqual(manifest.function)
|
|
134
|
+
if (componentName === 'ForwardRefComponent') expect(manifest.function?.parameters).toEqual([{ dataType: 'text' }])
|
|
135
|
+
})
|
|
136
|
+
it('uses a valid fixture with resolved React types and imported aliases', () => {
|
|
137
|
+
expect(program.getSemanticDiagnostics().map((diagnostic) => diagnostic.messageText)).toEqual([])
|
|
138
|
+
})
|
|
139
|
+
it.each([
|
|
140
|
+
['onChange', 'text'],
|
|
141
|
+
['onValueChange', 'text'],
|
|
142
|
+
['onCheckedChange', 'booleanValue'],
|
|
143
|
+
['onNumberChange', 'number'],
|
|
144
|
+
])('maps %s from its signature rather than its name', (name, dataType) => {
|
|
145
|
+
expect(buildCallback(name)._unsafeUnwrap().function).toEqual({ parameters: [{ dataType }] })
|
|
146
|
+
})
|
|
147
|
+
it.each([
|
|
148
|
+
['onEmail', 'email'],
|
|
149
|
+
['onEmailAlias', 'email'],
|
|
150
|
+
['onPhone', 'phone'],
|
|
151
|
+
['onUrl', 'webUrl'],
|
|
152
|
+
['onLink', 'link'],
|
|
153
|
+
['onImage', 'image'],
|
|
154
|
+
])('preserves semantic Wix parameter type for %s', (name, dataType) => {
|
|
155
|
+
expect(buildCallback(name)._unsafeUnwrap().function).toEqual({ parameters: [{ dataType }] })
|
|
156
|
+
})
|
|
157
|
+
it('preserves branded return values, including promised results', () => {
|
|
158
|
+
expect(buildCallback('getEmail')._unsafeUnwrap().function).toEqual({ returns: { dataType: 'email' } })
|
|
159
|
+
expect(buildCallback('loadEmail')._unsafeUnwrap().function).toEqual({ async: true, returns: { dataType: 'email' } })
|
|
160
|
+
expect(buildCallback('onContainer').isErr()).toBe(true)
|
|
161
|
+
})
|
|
162
|
+
it.each([
|
|
163
|
+
['onEvent', 'CHANGE'],
|
|
164
|
+
['onMouse', 'POINTER'],
|
|
165
|
+
['onPointer', 'POINTER'],
|
|
166
|
+
['onFocus', 'FOCUS'],
|
|
167
|
+
['onKeyboard', 'KEYBOARD'],
|
|
168
|
+
['onSubmit', 'SUBMIT'],
|
|
169
|
+
])('recognizes %s including imported event aliases', (name, domEvent) => {
|
|
170
|
+
expect(buildCallback(name)._unsafeUnwrap().function).toEqual({ parameters: [{ domEvent }] })
|
|
171
|
+
})
|
|
172
|
+
it('maps readonly value arrays with the function-specific item schema', () => {
|
|
173
|
+
expect(buildCallback('onSelectionChange')._unsafeUnwrap().function).toEqual({
|
|
174
|
+
parameters: [{ dataType: 'arrayItems', arrayItems: { item: { dataType: 'text' } } }],
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
it('handles optional callback props and optional boolean parameters', () => {
|
|
178
|
+
expect(buildCallback('onOptionalValueChange')._unsafeUnwrap().function).toEqual({
|
|
179
|
+
parameters: [{ dataType: 'text' }],
|
|
180
|
+
})
|
|
181
|
+
expect(buildCallback('onUndefinedCallback')._unsafeUnwrap().function).toEqual({
|
|
182
|
+
parameters: [{ dataType: 'text' }],
|
|
183
|
+
})
|
|
184
|
+
expect(buildCallback('onOptionalBoolean')._unsafeUnwrap().function).toEqual({
|
|
185
|
+
parameters: [{ dataType: 'booleanValue', optional: true }],
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
it('does not override a declared value signature with its DOM wiring', () => {
|
|
189
|
+
const propUsages: TrackingStores['propUsages'] = new Map([
|
|
190
|
+
[
|
|
191
|
+
'props.onValueChange',
|
|
192
|
+
{
|
|
193
|
+
elements: new Map([['input', { tag: 'input', elementId: 'input' }]]),
|
|
194
|
+
attributes: new Map([['input:onChange', { attr: 'onChange', concatenated: false }]]),
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
])
|
|
198
|
+
expect(buildCallback('onValueChange', 'onValueChange', propUsages)._unsafeUnwrap().function).toEqual({
|
|
199
|
+
parameters: [{ dataType: 'text' }],
|
|
200
|
+
})
|
|
201
|
+
})
|
|
202
|
+
it('preserves ordered parameters and optional structured fields', () => {
|
|
203
|
+
expect(buildCallback('onDetails')._unsafeUnwrap().function).toEqual({
|
|
204
|
+
parameters: [
|
|
205
|
+
{ dataType: 'text' },
|
|
206
|
+
{
|
|
207
|
+
dataType: 'data',
|
|
208
|
+
optional: true,
|
|
209
|
+
data: {
|
|
210
|
+
items: {
|
|
211
|
+
committed: { dataType: 'booleanValue' },
|
|
212
|
+
count: { dataType: 'number', optional: true },
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
},
|
|
216
|
+
],
|
|
217
|
+
})
|
|
218
|
+
})
|
|
219
|
+
it('does not invent an event parameter for a no-argument onClick', () => {
|
|
220
|
+
expect(buildCallback('onClick')._unsafeUnwrap().function).toEqual({})
|
|
221
|
+
})
|
|
222
|
+
it('keeps name inference for an uninformative legacy event signature', () => {
|
|
223
|
+
expect(buildCallback('onClickLegacy', 'onClick')._unsafeUnwrap().function).toEqual({
|
|
224
|
+
parameters: [{ domEvent: 'POINTER' }],
|
|
225
|
+
})
|
|
226
|
+
})
|
|
227
|
+
it('maps promised results and promised void', () => {
|
|
228
|
+
expect(buildCallback('calculate')._unsafeUnwrap().function).toEqual({
|
|
229
|
+
parameters: [{ dataType: 'number' }],
|
|
230
|
+
async: true,
|
|
231
|
+
returns: { dataType: 'number' },
|
|
232
|
+
})
|
|
233
|
+
expect(buildCallback('onLoad')._unsafeUnwrap().function).toEqual({ async: true })
|
|
234
|
+
})
|
|
235
|
+
it('preserves a string union as a text enum', () => {
|
|
236
|
+
expect(buildCallback('onMode')._unsafeUnwrap().function?.parameters?.[0]).toEqual({
|
|
237
|
+
dataType: 'textEnum',
|
|
238
|
+
textEnum: { options: [{ value: 'on' }, { value: 'off' }] },
|
|
239
|
+
})
|
|
240
|
+
})
|
|
241
|
+
it.each(['onCallbackUnion', 'onNullableCallback', 'onMixedCallback', 'onOptionalNullableCallback'])(
|
|
242
|
+
'rejects unsupported callback prop union %s',
|
|
243
|
+
(name) => {
|
|
244
|
+
const result = buildCallback(name)
|
|
245
|
+
expect(result.isErr()).toBe(true)
|
|
246
|
+
expect(result._unsafeUnwrapErr().message).toContain(name)
|
|
247
|
+
},
|
|
248
|
+
)
|
|
249
|
+
it.each(['onUnsupported', 'onNullable', 'onRest', 'onUnknown', 'overloaded', 'onTooMany'])(
|
|
250
|
+
'reports %s instead of dropping its parameters',
|
|
251
|
+
(name) => {
|
|
252
|
+
const result = buildCallback(name)
|
|
253
|
+
expect(result.isErr()).toBe(true)
|
|
254
|
+
expect(result._unsafeUnwrapErr().message).toContain(name)
|
|
255
|
+
},
|
|
256
|
+
)
|
|
257
|
+
})
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import type { DataItem, DomEvent } from '@wix/react-component-schema'
|
|
2
|
+
import { DATA, DOM_EVENT } from '@wix/react-component-schema'
|
|
3
|
+
import { type Result, err, ok } from 'neverthrow'
|
|
4
|
+
import { ParseError } from '../errors'
|
|
5
|
+
import type { ResolvedType } from '../information-extractors/ts/types'
|
|
6
|
+
import { WIX_TYPE_TO_DATA_TYPE } from '../wix-type-to-data-type'
|
|
7
|
+
|
|
8
|
+
type FunctionDefinition = NonNullable<DataItem['function']>
|
|
9
|
+
type FunctionParameter = NonNullable<FunctionDefinition['parameters']>[number]
|
|
10
|
+
type ParseErrorInstance = InstanceType<typeof ParseError>
|
|
11
|
+
|
|
12
|
+
const EVENT_TYPES: Record<string, DomEvent> = {
|
|
13
|
+
ChangeEvent: DOM_EVENT.CHANGE,
|
|
14
|
+
MouseEvent: DOM_EVENT.POINTER,
|
|
15
|
+
PointerEvent: DOM_EVENT.POINTER,
|
|
16
|
+
FocusEvent: DOM_EVENT.FOCUS,
|
|
17
|
+
KeyboardEvent: DOM_EVENT.KEYBOARD,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function unsupported(path: string): Result<never, ParseErrorInstance> {
|
|
21
|
+
return err(
|
|
22
|
+
new ParseError(`Cannot represent function signature at ${path}; use a supported explicit parameter/return type.`),
|
|
23
|
+
)
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function buildParameter(
|
|
27
|
+
type: ResolvedType,
|
|
28
|
+
path: string,
|
|
29
|
+
submitHandler = false,
|
|
30
|
+
): Result<FunctionParameter, ParseErrorInstance> {
|
|
31
|
+
if (type.kind === 'semantic' && (type.source === '@types/react' || type.source === 'react')) {
|
|
32
|
+
const domEvent = type.value === 'FormEvent' && submitHandler ? DOM_EVENT.SUBMIT : EVENT_TYPES[String(type.value)]
|
|
33
|
+
return domEvent ? ok({ domEvent } as unknown as FunctionParameter) : unsupported(path)
|
|
34
|
+
}
|
|
35
|
+
if (type.kind === 'semantic' && type.source === '@wix/editor-react-types') {
|
|
36
|
+
const dataType = WIX_TYPE_TO_DATA_TYPE[String(type.value)]
|
|
37
|
+
if (dataType && !['container', 'schema', 'arrayItems', 'textEnum'].includes(dataType)) {
|
|
38
|
+
return ok({ dataType: DATA.DATA_TYPE[dataType] })
|
|
39
|
+
}
|
|
40
|
+
return unsupported(path)
|
|
41
|
+
}
|
|
42
|
+
if (type.kind === 'primitive' || type.kind === 'literal') {
|
|
43
|
+
const primitive = type.kind === 'literal' ? typeof type.value : type.value
|
|
44
|
+
switch (primitive) {
|
|
45
|
+
case 'string':
|
|
46
|
+
return ok({ dataType: DATA.DATA_TYPE.text })
|
|
47
|
+
case 'number':
|
|
48
|
+
return ok({ dataType: DATA.DATA_TYPE.number })
|
|
49
|
+
case 'boolean':
|
|
50
|
+
return ok({ dataType: DATA.DATA_TYPE.booleanValue })
|
|
51
|
+
default:
|
|
52
|
+
return unsupported(path)
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
if (type.kind === 'union') {
|
|
56
|
+
// TypeScript represents boolean as true | false in some contexts.
|
|
57
|
+
const members = type.types ?? []
|
|
58
|
+
if (members.length && members.every((member) => member.kind === 'literal' && typeof member.value === 'boolean')) {
|
|
59
|
+
return ok({ dataType: DATA.DATA_TYPE.booleanValue })
|
|
60
|
+
}
|
|
61
|
+
if (members.length && members.every((member) => member.kind === 'literal' && typeof member.value === 'string')) {
|
|
62
|
+
return ok({
|
|
63
|
+
dataType: DATA.DATA_TYPE.textEnum,
|
|
64
|
+
textEnum: { options: members.map((member) => ({ value: String(member.value) })) },
|
|
65
|
+
})
|
|
66
|
+
}
|
|
67
|
+
return unsupported(path)
|
|
68
|
+
}
|
|
69
|
+
if (type.kind === 'array' && type.elementType) {
|
|
70
|
+
return buildParameter(type.elementType, `${path}[]`).map((item) => ({
|
|
71
|
+
dataType: DATA.DATA_TYPE.arrayItems,
|
|
72
|
+
arrayItems: { item },
|
|
73
|
+
}))
|
|
74
|
+
}
|
|
75
|
+
if (type.kind === 'object' && type.properties) {
|
|
76
|
+
const items: Record<string, FunctionParameter> = {}
|
|
77
|
+
for (const [name, property] of Object.entries(type.properties)) {
|
|
78
|
+
const propertyType =
|
|
79
|
+
!property.required && property.resolvedType.kind === 'union'
|
|
80
|
+
? property.resolvedType.types?.filter(
|
|
81
|
+
(member) => !(member.kind === 'primitive' && member.value === 'undefined'),
|
|
82
|
+
)
|
|
83
|
+
: undefined
|
|
84
|
+
const result = buildParameter(
|
|
85
|
+
propertyType
|
|
86
|
+
? propertyType.length === 1
|
|
87
|
+
? propertyType[0]
|
|
88
|
+
: { kind: 'union', types: propertyType }
|
|
89
|
+
: property.resolvedType,
|
|
90
|
+
`${path}.${name}`,
|
|
91
|
+
)
|
|
92
|
+
if (result.isErr()) return err(result.error)
|
|
93
|
+
items[name] = { ...result.value, ...(!property.required ? { optional: true } : {}) }
|
|
94
|
+
}
|
|
95
|
+
return ok({ dataType: DATA.DATA_TYPE.data, data: { items } })
|
|
96
|
+
}
|
|
97
|
+
if (type.kind === 'function') {
|
|
98
|
+
return buildFunctionSignature(type, path).andThen((definition) =>
|
|
99
|
+
definition ? ok({ dataType: DATA.DATA_TYPE.function, function: definition }) : unsupported(path),
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
return unsupported(path)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** Undefined means legacy metadata has no signature and name/usage inference may run. */
|
|
106
|
+
export function buildFunctionSignature(
|
|
107
|
+
type: ResolvedType,
|
|
108
|
+
path: string,
|
|
109
|
+
): Result<FunctionDefinition | undefined, ParseErrorInstance> {
|
|
110
|
+
const signature = type.functionSignature
|
|
111
|
+
if (!signature) return ok(undefined)
|
|
112
|
+
if (signature.overloaded || signature.parameters.length > 10) return unsupported(path)
|
|
113
|
+
if (
|
|
114
|
+
signature.parameters.length === 1 &&
|
|
115
|
+
!signature.parameters[0].rest &&
|
|
116
|
+
signature.parameters[0].type.kind === 'primitive' &&
|
|
117
|
+
['any', 'unknown'].includes(String(signature.parameters[0].type.value)) &&
|
|
118
|
+
signature.returnType.kind === 'primitive' &&
|
|
119
|
+
signature.returnType.value === 'void' &&
|
|
120
|
+
!signature.async
|
|
121
|
+
) {
|
|
122
|
+
return ok(undefined)
|
|
123
|
+
}
|
|
124
|
+
const parameters: FunctionParameter[] = []
|
|
125
|
+
let optionalParameterSeen = false
|
|
126
|
+
for (const parameter of signature.parameters) {
|
|
127
|
+
if (parameter.rest) return unsupported(`${path}.${parameter.name} (rest parameter)`)
|
|
128
|
+
if (optionalParameterSeen && !parameter.optional)
|
|
129
|
+
return unsupported(`${path}.${parameter.name} (required after optional)`)
|
|
130
|
+
optionalParameterSeen ||= parameter.optional
|
|
131
|
+
const result = buildParameter(parameter.type, `${path}.${parameter.name}`, path.toLowerCase() === 'onsubmit')
|
|
132
|
+
if (result.isErr()) return err(result.error)
|
|
133
|
+
parameters.push({ ...result.value, ...(parameter.optional ? { optional: true } : {}) })
|
|
134
|
+
}
|
|
135
|
+
const definition: FunctionDefinition = parameters.length ? { parameters } : {}
|
|
136
|
+
if (signature.async) definition.async = true
|
|
137
|
+
if (!(signature.returnType.kind === 'primitive' && signature.returnType.value === 'void')) {
|
|
138
|
+
const result = buildParameter(signature.returnType, `${path} return`)
|
|
139
|
+
if (result.isErr()) return err(result.error)
|
|
140
|
+
if (result.value.domEvent || result.value.function) return unsupported(`${path} return`)
|
|
141
|
+
definition.returns = result.value
|
|
142
|
+
}
|
|
143
|
+
return ok(definition)
|
|
144
|
+
}
|
|
@@ -21,6 +21,12 @@ export interface ResolvedType {
|
|
|
21
21
|
properties?: Record<string, PropInfo>
|
|
22
22
|
elementType?: ResolvedType
|
|
23
23
|
types?: ResolvedType[] // For unions and intersections
|
|
24
|
+
functionSignature?: {
|
|
25
|
+
parameters: { name: string; type: ResolvedType; optional: boolean; rest: boolean }[]
|
|
26
|
+
returnType: ResolvedType
|
|
27
|
+
async: boolean
|
|
28
|
+
overloaded: boolean
|
|
29
|
+
}
|
|
24
30
|
}
|
|
25
31
|
|
|
26
32
|
/**
|
|
@@ -214,7 +214,7 @@ function resolveTypeBody({
|
|
|
214
214
|
() => resolveUnionType(type, checker, depth, seenTypes),
|
|
215
215
|
() => resolveArrayType(type, checker, depth, seenTypes),
|
|
216
216
|
() => resolveIntersectionType(type, checker, depth, seenTypes),
|
|
217
|
-
() => resolveFunctionType(type, checker),
|
|
217
|
+
() => resolveFunctionType(type, checker, depth, seenTypes),
|
|
218
218
|
() => resolveObjectType(type, checker, depth, seenTypes),
|
|
219
219
|
]
|
|
220
220
|
|
|
@@ -367,7 +367,58 @@ function getArrayElementType(type: ts.Type, checker: ts.TypeChecker): ts.Type |
|
|
|
367
367
|
return checker.getIndexTypeOfType(type, ts.IndexKind.Number)
|
|
368
368
|
}
|
|
369
369
|
|
|
370
|
-
|
|
370
|
+
const REACT_EVENT_TYPES = new Set([
|
|
371
|
+
'ChangeEvent',
|
|
372
|
+
'MouseEvent',
|
|
373
|
+
'PointerEvent',
|
|
374
|
+
'FocusEvent',
|
|
375
|
+
'KeyboardEvent',
|
|
376
|
+
'FormEvent',
|
|
377
|
+
])
|
|
378
|
+
|
|
379
|
+
function resolveCallbackType(
|
|
380
|
+
type: ts.Type,
|
|
381
|
+
checker: ts.TypeChecker,
|
|
382
|
+
depth: number,
|
|
383
|
+
seenTypes: Set<ts.Type>,
|
|
384
|
+
typeNode?: ts.TypeNode,
|
|
385
|
+
): ResolvedType {
|
|
386
|
+
const declaredSemanticType = resolveDeclaredCallbackSemanticType(typeNode, checker)
|
|
387
|
+
if (declaredSemanticType) return declaredSemanticType
|
|
388
|
+
const symbol = type.getSymbol()
|
|
389
|
+
if (symbol && REACT_EVENT_TYPES.has(symbol.getName())) {
|
|
390
|
+
const source = getSymbolPackage(symbol, checker)
|
|
391
|
+
if (source === '@types/react' || source === 'react') {
|
|
392
|
+
return { kind: 'semantic', value: symbol.getName(), source }
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
return resolveType({ type, checker, depth: depth + 1, seenTypes })
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function resolveDeclaredCallbackSemanticType(
|
|
399
|
+
typeNode: ts.TypeNode | undefined,
|
|
400
|
+
checker: ts.TypeChecker,
|
|
401
|
+
seenSymbols = new Set<ts.Symbol>(),
|
|
402
|
+
): ResolvedType | undefined {
|
|
403
|
+
if (!typeNode || !ts.isTypeReferenceNode(typeNode)) return undefined
|
|
404
|
+
let symbol = checker.getSymbolAtLocation(typeNode.typeName)
|
|
405
|
+
if (!symbol) return undefined
|
|
406
|
+
if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) symbol = checker.getAliasedSymbol(symbol)
|
|
407
|
+
if (seenSymbols.has(symbol)) return undefined
|
|
408
|
+
seenSymbols.add(symbol)
|
|
409
|
+
if (getSymbolPackage(symbol, checker) === '@wix/editor-react-types' && isValidWixSemanticType(symbol.getName())) {
|
|
410
|
+
return { kind: 'semantic', value: symbol.getName(), source: '@wix/editor-react-types' }
|
|
411
|
+
}
|
|
412
|
+
const declaration = symbol.getDeclarations()?.find(ts.isTypeAliasDeclaration)
|
|
413
|
+
return declaration ? resolveDeclaredCallbackSemanticType(declaration.type, checker, seenSymbols) : undefined
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
function resolveFunctionType(
|
|
417
|
+
type: ts.Type,
|
|
418
|
+
checker: ts.TypeChecker,
|
|
419
|
+
depth: number,
|
|
420
|
+
seenTypes: Set<ts.Type>,
|
|
421
|
+
): ResolvedType | undefined {
|
|
371
422
|
const callSignatures = type.getCallSignatures()
|
|
372
423
|
if (callSignatures.length > 0) {
|
|
373
424
|
const signature = callSignatures[0]
|
|
@@ -379,6 +430,13 @@ function resolveFunctionType(type: ts.Type, checker: ts.TypeChecker): ResolvedTy
|
|
|
379
430
|
}
|
|
380
431
|
})
|
|
381
432
|
const returnType = signature.getReturnType()
|
|
433
|
+
const isAsync = returnType.getSymbol()?.getName() === 'Promise'
|
|
434
|
+
const resolvedReturnType = isAsync ? checker.getTypeArguments(returnType as ts.TypeReference)[0] : returnType
|
|
435
|
+
const declaredReturnType = signature.getDeclaration()?.type
|
|
436
|
+
const returnTypeNode =
|
|
437
|
+
isAsync && declaredReturnType && ts.isTypeReferenceNode(declaredReturnType)
|
|
438
|
+
? declaredReturnType.typeArguments?.[0]
|
|
439
|
+
: declaredReturnType
|
|
382
440
|
|
|
383
441
|
return {
|
|
384
442
|
kind: 'function',
|
|
@@ -386,6 +444,39 @@ function resolveFunctionType(type: ts.Type, checker: ts.TypeChecker): ResolvedTy
|
|
|
386
444
|
parameters,
|
|
387
445
|
returnType: checker.typeToString(returnType),
|
|
388
446
|
},
|
|
447
|
+
functionSignature: {
|
|
448
|
+
parameters: signature.getParameters().map((parameter) => {
|
|
449
|
+
const declaration = parameter.valueDeclaration
|
|
450
|
+
const parameterTypeNode = declaration && ts.isParameter(declaration) ? declaration.type : undefined
|
|
451
|
+
const parameterType = checker.getTypeOfSymbol(parameter)
|
|
452
|
+
const optional =
|
|
453
|
+
(parameter.flags & ts.SymbolFlags.Optional) !== 0 ||
|
|
454
|
+
(declaration &&
|
|
455
|
+
ts.isParameter(declaration) &&
|
|
456
|
+
Boolean(declaration.questionToken || declaration.initializer))
|
|
457
|
+
const definedType =
|
|
458
|
+
optional && parameterType.isUnion()
|
|
459
|
+
? parameterType.types.filter((member) => !(member.flags & ts.TypeFlags.Undefined))
|
|
460
|
+
: [parameterType]
|
|
461
|
+
return {
|
|
462
|
+
name: parameter.getName(),
|
|
463
|
+
type:
|
|
464
|
+
definedType.length === 1
|
|
465
|
+
? resolveCallbackType(definedType[0], checker, depth, seenTypes, parameterTypeNode)
|
|
466
|
+
: {
|
|
467
|
+
kind: 'union',
|
|
468
|
+
types: definedType.map((member) =>
|
|
469
|
+
resolveCallbackType(member, checker, depth, seenTypes, parameterTypeNode),
|
|
470
|
+
),
|
|
471
|
+
},
|
|
472
|
+
optional: Boolean(optional),
|
|
473
|
+
rest: Boolean(declaration && ts.isParameter(declaration) && declaration.dotDotDotToken),
|
|
474
|
+
}
|
|
475
|
+
}),
|
|
476
|
+
returnType: resolveCallbackType(resolvedReturnType ?? returnType, checker, depth, seenTypes, returnTypeNode),
|
|
477
|
+
async: isAsync,
|
|
478
|
+
overloaded: callSignatures.length > 1,
|
|
479
|
+
},
|
|
389
480
|
}
|
|
390
481
|
}
|
|
391
482
|
return undefined
|