@vxrn/compiler 1.25.13 → 1.25.16-1788753284749
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/cjs/cache.cjs +3 -1
- package/dist/cjs/configure.cjs +9 -1
- package/dist/cjs/index.cjs +82 -4
- package/dist/cjs/reactNativeCodegen.cjs +140 -0
- package/dist/cjs/reactNativeViewConfig.cjs +489 -0
- package/dist/cjs/transformBabel.cjs +15 -20
- package/dist/cjs/transformBabel.test.cjs +260 -0
- package/dist/cjs/transformHermesLoops.cjs +317 -0
- package/dist/cjs/transformHermesLoops.test.cjs +220 -0
- package/dist/cjs/transformSWC.cjs +102 -137
- package/dist/cjs/transformSWC.test.cjs +74 -0
- package/dist/cjs/transformWorklets.cjs +121 -0
- package/dist/cjs/transformWorklets.test.cjs +604 -0
- package/dist/cjs/worklets/autoworklet.cjs +265 -0
- package/dist/cjs/worklets/globals.cjs +71 -0
- package/dist/cjs/worklets/hash.cjs +38 -0
- package/dist/cjs/worklets/scope.cjs +370 -0
- package/dist/cjs/worklets/serialize.cjs +101 -0
- package/dist/cjs/worklets/transform.cjs +232 -0
- package/dist/cjs/worklets/types.cjs +18 -0
- package/dist/esm/cache.mjs +4 -2
- package/dist/esm/cache.mjs.map +1 -1
- package/dist/esm/configure.mjs +8 -1
- package/dist/esm/configure.mjs.map +1 -1
- package/dist/esm/index.js +72 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/index.mjs +72 -5
- package/dist/esm/index.mjs.map +1 -1
- package/dist/esm/reactNativeCodegen.mjs +104 -0
- package/dist/esm/reactNativeCodegen.mjs.map +1 -0
- package/dist/esm/reactNativeViewConfig.mjs +460 -0
- package/dist/esm/reactNativeViewConfig.mjs.map +1 -0
- package/dist/esm/transformBabel.mjs +14 -19
- package/dist/esm/transformBabel.mjs.map +1 -1
- package/dist/esm/transformBabel.test.mjs +261 -1
- package/dist/esm/transformBabel.test.mjs.map +1 -1
- package/dist/esm/transformHermesLoops.mjs +281 -0
- package/dist/esm/transformHermesLoops.mjs.map +1 -0
- package/dist/esm/transformHermesLoops.test.mjs +197 -0
- package/dist/esm/transformHermesLoops.test.mjs.map +1 -0
- package/dist/esm/transformSWC.mjs +89 -138
- package/dist/esm/transformSWC.mjs.map +1 -1
- package/dist/esm/transformSWC.test.mjs +75 -0
- package/dist/esm/transformSWC.test.mjs.map +1 -0
- package/dist/esm/transformWorklets.mjs +81 -0
- package/dist/esm/transformWorklets.mjs.map +1 -0
- package/dist/esm/transformWorklets.test.mjs +581 -0
- package/dist/esm/transformWorklets.test.mjs.map +1 -0
- package/dist/esm/worklets/autoworklet.mjs +234 -0
- package/dist/esm/worklets/autoworklet.mjs.map +1 -0
- package/dist/esm/worklets/globals.mjs +45 -0
- package/dist/esm/worklets/globals.mjs.map +1 -0
- package/dist/esm/worklets/hash.mjs +13 -0
- package/dist/esm/worklets/hash.mjs.map +1 -0
- package/dist/esm/worklets/scope.mjs +345 -0
- package/dist/esm/worklets/scope.mjs.map +1 -0
- package/dist/esm/worklets/serialize.mjs +75 -0
- package/dist/esm/worklets/serialize.mjs.map +1 -0
- package/dist/esm/worklets/transform.mjs +196 -0
- package/dist/esm/worklets/transform.mjs.map +1 -0
- package/dist/esm/worklets/types.mjs +2 -0
- package/dist/esm/worklets/types.mjs.map +1 -0
- package/package.json +10 -6
- package/src/cache.ts +9 -2
- package/src/configure.ts +12 -0
- package/src/index.ts +93 -13
- package/src/reactNativeCodegen.ts +181 -0
- package/src/reactNativeViewConfig.ts +593 -0
- package/src/transformBabel.test.ts +268 -1
- package/src/transformBabel.ts +21 -31
- package/src/transformHermesLoops.test.ts +282 -0
- package/src/transformHermesLoops.ts +433 -0
- package/src/transformSWC.test.ts +88 -0
- package/src/transformSWC.ts +109 -160
- package/src/transformWorklets.test.ts +708 -0
- package/src/transformWorklets.ts +160 -0
- package/src/types.ts +4 -4
- package/src/worklets/autoworklet.ts +391 -0
- package/src/worklets/globals.ts +125 -0
- package/src/worklets/hash.ts +14 -0
- package/src/worklets/scope.ts +401 -0
- package/src/worklets/serialize.ts +114 -0
- package/src/worklets/transform.ts +282 -0
- package/src/worklets/types.ts +26 -0
- package/types/cache.d.ts +1 -0
- package/types/cache.d.ts.map +1 -1
- package/types/configure.d.ts +2 -0
- package/types/configure.d.ts.map +1 -1
- package/types/index.d.ts +3 -0
- package/types/index.d.ts.map +1 -1
- package/types/reactNativeCodegen.d.ts +5 -0
- package/types/reactNativeCodegen.d.ts.map +1 -0
- package/types/reactNativeViewConfig.d.ts +35 -0
- package/types/reactNativeViewConfig.d.ts.map +1 -0
- package/types/transformBabel.d.ts +3 -3
- package/types/transformBabel.d.ts.map +1 -1
- package/types/transformHermesLoops.d.ts +5 -0
- package/types/transformHermesLoops.d.ts.map +1 -0
- package/types/transformHermesLoops.test.d.ts +2 -0
- package/types/transformHermesLoops.test.d.ts.map +1 -0
- package/types/transformSWC.d.ts +12 -3
- package/types/transformSWC.d.ts.map +1 -1
- package/types/transformSWC.test.d.ts +2 -0
- package/types/transformSWC.test.d.ts.map +1 -0
- package/types/transformWorklets.d.ts +22 -0
- package/types/transformWorklets.d.ts.map +1 -0
- package/types/transformWorklets.test.d.ts +2 -0
- package/types/transformWorklets.test.d.ts.map +1 -0
- package/types/types.d.ts +4 -4
- package/types/types.d.ts.map +1 -1
- package/types/worklets/autoworklet.d.ts +14 -0
- package/types/worklets/autoworklet.d.ts.map +1 -0
- package/types/worklets/globals.d.ts +3 -0
- package/types/worklets/globals.d.ts.map +1 -0
- package/types/worklets/hash.d.ts +5 -0
- package/types/worklets/hash.d.ts.map +1 -0
- package/types/worklets/scope.d.ts +7 -0
- package/types/worklets/scope.d.ts.map +1 -0
- package/types/worklets/serialize.d.ts +12 -0
- package/types/worklets/serialize.d.ts.map +1 -0
- package/types/worklets/transform.d.ts +6 -0
- package/types/worklets/transform.d.ts.map +1 -0
- package/types/worklets/types.d.ts +21 -0
- package/types/worklets/types.d.ts.map +1 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* View-config generation for react-native component specs, without babel.
|
|
3
|
+
*
|
|
4
|
+
* `@react-native/codegen` builds a full CodegenSchema with @babel/parser and
|
|
5
|
+
* emits the module with @babel/generator, which is the last thing in the native
|
|
6
|
+
* pipeline that runs babel at all: 11,774 babel calls per bundle of the soot
|
|
7
|
+
* app, every one of them from codegen. The view config itself is a plain object
|
|
8
|
+
* literal, so it can be read off the spec's own AST and printed as text.
|
|
9
|
+
*
|
|
10
|
+
* Only what a view config needs is modeled here: the prop list in declaration
|
|
11
|
+
* order, the five prop types that get a custom attribute descriptor, the two
|
|
12
|
+
* event handler shapes, and the command list. Everything else about a prop's
|
|
13
|
+
* type is irrelevant to the runtime, which is why the generated `validAttributes`
|
|
14
|
+
* is almost entirely `true`.
|
|
15
|
+
*
|
|
16
|
+
* TypeScript specs are parsed with oxc. React Native's own specs are Flow, which
|
|
17
|
+
* oxc cannot parse, so those go through hermes-parser (which is not babel).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { createRequire } from 'node:module'
|
|
21
|
+
import { basename, join } from 'node:path'
|
|
22
|
+
import { parseSync } from 'oxc-parser'
|
|
23
|
+
|
|
24
|
+
const SELF_URL: string = typeof __filename !== 'undefined' ? __filename : import.meta.url
|
|
25
|
+
|
|
26
|
+
/** attribute descriptors react-native needs for props it cannot diff by identity */
|
|
27
|
+
const COLOR_ATTRIBUTE =
|
|
28
|
+
"require('react-native/Libraries/Components/View/ReactNativeStyleAttributes').colorAttribute"
|
|
29
|
+
const COLOR_ARRAY_ATTRIBUTE =
|
|
30
|
+
"{\n process: (req => 'default' in req ? req.default : req)(require('react-native/Libraries/StyleSheet/processColorArray'))\n }"
|
|
31
|
+
const IMAGE_ATTRIBUTE =
|
|
32
|
+
"{\n process: (req => 'default' in req ? req.default : req)(require('react-native/Libraries/Image/resolveAssetSource'))\n }"
|
|
33
|
+
const POINT_ATTRIBUTE =
|
|
34
|
+
"{\n diff: (req => 'default' in req ? req.default : req)(require('react-native/Libraries/Utilities/differ/pointsDiffer'))\n }"
|
|
35
|
+
const EDGE_INSETS_ATTRIBUTE =
|
|
36
|
+
"{\n diff: (req => 'default' in req ? req.default : req)(require('react-native/Libraries/Utilities/differ/insetsDiffer'))\n }"
|
|
37
|
+
|
|
38
|
+
const IMPORT_REGISTRY =
|
|
39
|
+
"const NativeComponentRegistry = require('react-native/Libraries/NativeComponent/NativeComponentRegistry');"
|
|
40
|
+
const IMPORT_IGNORE =
|
|
41
|
+
"const {ConditionallyIgnoredEventHandlers} = require('react-native/Libraries/NativeComponent/ViewConfigIgnore');"
|
|
42
|
+
const IMPORT_DISPATCH =
|
|
43
|
+
'const {dispatchCommand} = require("react-native/Libraries/ReactNative/RendererProxy");'
|
|
44
|
+
const IMPORT_UIMANAGER = 'const {UIManager} = require("react-native")'
|
|
45
|
+
|
|
46
|
+
type Attribute = { name: string; value: string }
|
|
47
|
+
type EventProp = { name: string; bubbling: boolean; topName: string }
|
|
48
|
+
type Command = { name: string; params: string[] }
|
|
49
|
+
|
|
50
|
+
/** a prop or event read off the spec, before it becomes view-config text */
|
|
51
|
+
type Member = { name: string; type: TypeRef }
|
|
52
|
+
|
|
53
|
+
/** a type annotation reduced to the only two things that matter here */
|
|
54
|
+
type TypeRef = { name: string; args: TypeRef[] }
|
|
55
|
+
|
|
56
|
+
let hermesParser: any = undefined
|
|
57
|
+
|
|
58
|
+
function getHermesParser(projectRoot?: string) {
|
|
59
|
+
if (hermesParser === undefined) {
|
|
60
|
+
try {
|
|
61
|
+
const req = projectRoot
|
|
62
|
+
? createRequire(join(projectRoot, 'package.json'))
|
|
63
|
+
: createRequire(SELF_URL)
|
|
64
|
+
hermesParser = req('hermes-parser')
|
|
65
|
+
} catch {
|
|
66
|
+
try {
|
|
67
|
+
hermesParser = createRequire(SELF_URL)('hermes-parser')
|
|
68
|
+
} catch {
|
|
69
|
+
hermesParser = null
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return hermesParser
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ---------------------------------------------------------------------------
|
|
77
|
+
// type annotations
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Both dialects reduce to a name plus type arguments. Wrappers that do not
|
|
82
|
+
* change what the runtime does with a prop are unwrapped here rather than
|
|
83
|
+
* modeled: a nullable color is still a color.
|
|
84
|
+
*/
|
|
85
|
+
function readType(node: any): TypeRef {
|
|
86
|
+
if (!node) return { name: '', args: [] }
|
|
87
|
+
|
|
88
|
+
switch (node.type) {
|
|
89
|
+
// flow
|
|
90
|
+
case 'NullableTypeAnnotation':
|
|
91
|
+
case 'TypeAnnotation':
|
|
92
|
+
return readType(node.typeAnnotation)
|
|
93
|
+
case 'GenericTypeAnnotation':
|
|
94
|
+
return {
|
|
95
|
+
name: readTypeName(node.id),
|
|
96
|
+
args: (node.typeParameters?.params ?? []).map(readType),
|
|
97
|
+
}
|
|
98
|
+
case 'ObjectTypeAnnotation':
|
|
99
|
+
return { name: '__object', args: [] }
|
|
100
|
+
case 'StringTypeAnnotation':
|
|
101
|
+
return { name: 'string', args: [] }
|
|
102
|
+
case 'BooleanTypeAnnotation':
|
|
103
|
+
return { name: 'boolean', args: [] }
|
|
104
|
+
case 'NumberTypeAnnotation':
|
|
105
|
+
return { name: 'number', args: [] }
|
|
106
|
+
case 'UnionTypeAnnotation':
|
|
107
|
+
return readUnion(node)
|
|
108
|
+
case 'FunctionTypeAnnotation':
|
|
109
|
+
return { name: '__function', args: [] }
|
|
110
|
+
|
|
111
|
+
// typescript
|
|
112
|
+
case 'TSTypeAnnotation':
|
|
113
|
+
return readType(node.typeAnnotation)
|
|
114
|
+
case 'TSTypeReference':
|
|
115
|
+
return {
|
|
116
|
+
name: readTypeName(node.typeName),
|
|
117
|
+
args: (node.typeArguments ?? node.typeParameters)?.params?.map(readType) ?? [],
|
|
118
|
+
}
|
|
119
|
+
case 'TSTypeLiteral':
|
|
120
|
+
return { name: '__object', args: [] }
|
|
121
|
+
case 'TSStringKeyword':
|
|
122
|
+
return { name: 'string', args: [] }
|
|
123
|
+
case 'TSBooleanKeyword':
|
|
124
|
+
return { name: 'boolean', args: [] }
|
|
125
|
+
case 'TSNumberKeyword':
|
|
126
|
+
return { name: 'number', args: [] }
|
|
127
|
+
case 'TSUnionType':
|
|
128
|
+
return readUnion(node)
|
|
129
|
+
case 'TSFunctionType':
|
|
130
|
+
return { name: '__function', args: [] }
|
|
131
|
+
case 'TSArrayType':
|
|
132
|
+
return { name: 'Array', args: [readType(node.elementType)] }
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { name: '__unknown', args: [] }
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const NULLISH_TYPES = new Set([
|
|
139
|
+
'TSUndefinedKeyword',
|
|
140
|
+
'TSNullKeyword',
|
|
141
|
+
'TSVoidKeyword',
|
|
142
|
+
'NullLiteralTypeAnnotation',
|
|
143
|
+
'VoidTypeAnnotation',
|
|
144
|
+
])
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* `DirectEventHandler<E> | undefined` is the same prop as `DirectEventHandler<E>`.
|
|
148
|
+
* A union of anything else is an enum or a mixed type, which the runtime diffs
|
|
149
|
+
* by identity either way.
|
|
150
|
+
*/
|
|
151
|
+
function readUnion(node: any): TypeRef {
|
|
152
|
+
const real = (node.types ?? []).filter((t: any) => !NULLISH_TYPES.has(t.type))
|
|
153
|
+
if (real.length === 1) return readType(real[0])
|
|
154
|
+
return { name: '__union', args: [] }
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function readTypeName(id: any): string {
|
|
158
|
+
if (!id) return ''
|
|
159
|
+
if (id.type === 'Identifier') return id.name
|
|
160
|
+
// Qualified names (`Foo.Bar`) only appear on types the runtime treats as
|
|
161
|
+
// opaque, so the last segment is enough to classify them.
|
|
162
|
+
if (id.type === 'QualifiedTypeIdentifier') return readTypeName(id.id)
|
|
163
|
+
if (id.type === 'TSQualifiedName') return readTypeName(id.right)
|
|
164
|
+
return ''
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const READONLY_ARRAYS = new Set(['ReadonlyArray', '$ReadOnlyArray', 'Array'])
|
|
168
|
+
|
|
169
|
+
/** wrappers that carry no runtime meaning for a view config */
|
|
170
|
+
const TRANSPARENT = new Set(['WithDefault', 'UnsafeMixed', 'Readonly', '$ReadOnly'])
|
|
171
|
+
|
|
172
|
+
function unwrap(type: TypeRef): TypeRef {
|
|
173
|
+
let cur = type
|
|
174
|
+
while (TRANSPARENT.has(cur.name) && cur.args.length > 0) {
|
|
175
|
+
cur = cur.args[0]
|
|
176
|
+
}
|
|
177
|
+
return cur
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function attributeValue(type: TypeRef): string {
|
|
181
|
+
const t = unwrap(type)
|
|
182
|
+
switch (t.name) {
|
|
183
|
+
case 'ColorValue':
|
|
184
|
+
case 'ProcessedColorValue':
|
|
185
|
+
return COLOR_ATTRIBUTE
|
|
186
|
+
case 'ImageSource':
|
|
187
|
+
return IMAGE_ATTRIBUTE
|
|
188
|
+
case 'PointValue':
|
|
189
|
+
return POINT_ATTRIBUTE
|
|
190
|
+
case 'EdgeInsetsValue':
|
|
191
|
+
return EDGE_INSETS_ATTRIBUTE
|
|
192
|
+
}
|
|
193
|
+
if (READONLY_ARRAYS.has(t.name) && t.args.length === 1) {
|
|
194
|
+
const el = unwrap(t.args[0])
|
|
195
|
+
if (el.name === 'ColorValue' || el.name === 'ProcessedColorValue') {
|
|
196
|
+
return COLOR_ARRAY_ATTRIBUTE
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return 'true'
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function eventOf(name: string, type: TypeRef): EventProp | null {
|
|
203
|
+
const t = unwrap(type)
|
|
204
|
+
if (t.name !== 'DirectEventHandler' && t.name !== 'BubblingEventHandler') return null
|
|
205
|
+
// the second type argument is the deprecated paper-level event name, which
|
|
206
|
+
// renames the top-level event but never the registration name
|
|
207
|
+
const override = t.args[1]?.name
|
|
208
|
+
return {
|
|
209
|
+
name,
|
|
210
|
+
bubbling: t.name === 'BubblingEventHandler',
|
|
211
|
+
topName: normalizeInputEventName(override && override !== '__unknown' ? override : name),
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
function normalizeInputEventName(name: string) {
|
|
216
|
+
if (name.startsWith('on')) return name.replace(/^on/, 'top')
|
|
217
|
+
if (!name.startsWith('top')) return `top${name[0].toUpperCase()}${name.slice(1)}`
|
|
218
|
+
return name
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
// reading the spec
|
|
223
|
+
// ---------------------------------------------------------------------------
|
|
224
|
+
|
|
225
|
+
type Declarations = Map<string, any>
|
|
226
|
+
|
|
227
|
+
function collectDeclarations(body: any[]): Declarations {
|
|
228
|
+
const out: Declarations = new Map()
|
|
229
|
+
for (const raw of body) {
|
|
230
|
+
const node = raw?.type === 'ExportNamedDeclaration' ? raw.declaration : raw
|
|
231
|
+
if (!node) continue
|
|
232
|
+
switch (node.type) {
|
|
233
|
+
case 'TSTypeAliasDeclaration':
|
|
234
|
+
case 'TypeAlias':
|
|
235
|
+
case 'TSInterfaceDeclaration':
|
|
236
|
+
case 'InterfaceDeclaration':
|
|
237
|
+
if (node.id?.name) out.set(node.id.name, node)
|
|
238
|
+
break
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return out
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* The members a props type contributes, in the order react-native's own codegen
|
|
246
|
+
* emits them: everything it extends first, in declaration order, then its own.
|
|
247
|
+
* `ViewProps` is the core view's props, which the runtime supplies itself.
|
|
248
|
+
*/
|
|
249
|
+
function flattenMembers(
|
|
250
|
+
typeName: string,
|
|
251
|
+
decls: Declarations,
|
|
252
|
+
seen = new Set<string>()
|
|
253
|
+
): Member[] {
|
|
254
|
+
if (typeName === 'ViewProps' || seen.has(typeName)) return []
|
|
255
|
+
seen.add(typeName)
|
|
256
|
+
|
|
257
|
+
const decl = decls.get(typeName)
|
|
258
|
+
if (!decl) return []
|
|
259
|
+
|
|
260
|
+
const out: Member[] = []
|
|
261
|
+
|
|
262
|
+
const heritage = decl.extends ?? decl.heritage ?? []
|
|
263
|
+
for (const h of heritage) {
|
|
264
|
+
const name = readTypeName(h.id ?? h.expression ?? h.typeName)
|
|
265
|
+
if (name) out.push(...flattenMembers(name, decls, seen))
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const bodyNode =
|
|
269
|
+
decl.body ?? // interface
|
|
270
|
+
decl.right ?? // type alias
|
|
271
|
+
decl.typeAnnotation
|
|
272
|
+
out.push(...readObjectMembers(bodyNode, decls, seen))
|
|
273
|
+
|
|
274
|
+
return out
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function readObjectMembers(node: any, decls: Declarations, seen: Set<string>): Member[] {
|
|
278
|
+
if (!node) return []
|
|
279
|
+
|
|
280
|
+
// `Readonly<{...}>` / `$ReadOnly<{...}>` around the real shape
|
|
281
|
+
if (node.type === 'TSTypeReference' || node.type === 'GenericTypeAnnotation') {
|
|
282
|
+
const name = readTypeName(node.typeName ?? node.id)
|
|
283
|
+
const args = (node.typeArguments ?? node.typeParameters)?.params ?? []
|
|
284
|
+
if (TRANSPARENT.has(name) && args.length) {
|
|
285
|
+
return readObjectMembers(args[0], decls, seen)
|
|
286
|
+
}
|
|
287
|
+
// an alias of another props type
|
|
288
|
+
return flattenMembers(name, decls, seen)
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
if (node.type === 'TSIntersectionType' || node.type === 'IntersectionTypeAnnotation') {
|
|
292
|
+
const out: Member[] = []
|
|
293
|
+
for (const t of node.types ?? []) out.push(...readObjectMembers(t, decls, seen))
|
|
294
|
+
return out
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
const members = node.members ?? node.body ?? node.properties ?? []
|
|
298
|
+
const out: Member[] = []
|
|
299
|
+
for (const m of members) {
|
|
300
|
+
switch (m.type) {
|
|
301
|
+
case 'TSPropertySignature':
|
|
302
|
+
case 'ObjectTypeProperty': {
|
|
303
|
+
const name = m.key?.name ?? m.key?.value
|
|
304
|
+
if (!name) break
|
|
305
|
+
out.push({ name, type: readType(m.typeAnnotation ?? m.value) })
|
|
306
|
+
break
|
|
307
|
+
}
|
|
308
|
+
// flow's `...OtherProps` inside an object type
|
|
309
|
+
case 'ObjectTypeSpreadProperty': {
|
|
310
|
+
const name = readTypeName(m.argument?.id ?? m.argument)
|
|
311
|
+
if (name) out.push(...flattenMembers(name, decls, seen))
|
|
312
|
+
break
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return out
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function findComponentCall(body: any[]): any {
|
|
320
|
+
for (const node of body) {
|
|
321
|
+
if (node.type !== 'ExportDefaultDeclaration') continue
|
|
322
|
+
let expr = node.declaration
|
|
323
|
+
while (expr) {
|
|
324
|
+
if (expr.type === 'CallExpression') break
|
|
325
|
+
expr =
|
|
326
|
+
expr.expression ??
|
|
327
|
+
(expr.type === 'SequenceExpression'
|
|
328
|
+
? expr.expressions[expr.expressions.length - 1]
|
|
329
|
+
: null)
|
|
330
|
+
}
|
|
331
|
+
if (expr?.type === 'CallExpression' && expr.callee?.name === 'codegenNativeComponent') {
|
|
332
|
+
return expr
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
return null
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function findCommandsCall(body: any[]): any {
|
|
339
|
+
for (const raw of body) {
|
|
340
|
+
const node = raw?.type === 'ExportNamedDeclaration' ? raw.declaration : raw
|
|
341
|
+
const decl = node?.declarations?.[0]
|
|
342
|
+
let init = decl?.init
|
|
343
|
+
while (init && init.type !== 'CallExpression') init = init.expression
|
|
344
|
+
if (init?.type === 'CallExpression' && init.callee?.name === 'codegenNativeCommands') {
|
|
345
|
+
return init
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return null
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function readStringOption(objectExpression: any, key: string): string | null {
|
|
352
|
+
for (const p of objectExpression?.properties ?? []) {
|
|
353
|
+
if ((p.key?.name ?? p.key?.value) === key && typeof p.value?.value === 'string') {
|
|
354
|
+
return p.value.value
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
return null
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function readCommands(call: any, decls: Declarations): Command[] {
|
|
361
|
+
const typeName = readTypeName(
|
|
362
|
+
(call.typeArguments ?? call.typeParameters)?.params?.[0]?.typeName ??
|
|
363
|
+
(call.typeArguments ?? call.typeParameters)?.params?.[0]?.id
|
|
364
|
+
)
|
|
365
|
+
const supported: string[] = []
|
|
366
|
+
for (const p of call.arguments?.[0]?.properties ?? []) {
|
|
367
|
+
if ((p.key?.name ?? p.key?.value) !== 'supportedCommands') continue
|
|
368
|
+
for (const el of p.value?.elements ?? []) {
|
|
369
|
+
if (typeof el?.value === 'string') supported.push(el.value)
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
if (!supported.length) return []
|
|
373
|
+
|
|
374
|
+
// parameter names come from the commands interface; the first parameter is
|
|
375
|
+
// the view ref, which the generated method takes but never forwards
|
|
376
|
+
const decl = decls.get(typeName)
|
|
377
|
+
const byName = new Map<string, string[]>()
|
|
378
|
+
// a TS interface body holds `.body`; flow's is an object type with `.properties`
|
|
379
|
+
const commandMembers =
|
|
380
|
+
decl?.body?.body ?? decl?.body?.members ?? decl?.body?.properties ?? []
|
|
381
|
+
for (const m of commandMembers) {
|
|
382
|
+
const name = m.key?.name ?? m.key?.value
|
|
383
|
+
if (!name) continue
|
|
384
|
+
const fn = m.typeAnnotation?.typeAnnotation ?? m.value ?? m.typeAnnotation
|
|
385
|
+
const params = (fn?.params ?? fn?.parameters ?? []).slice(1)
|
|
386
|
+
byName.set(
|
|
387
|
+
name,
|
|
388
|
+
params.map((p: any, i: number) => p.name?.name ?? p.name ?? `arg${i}`)
|
|
389
|
+
)
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return supported.map((name) => ({ name, params: byName.get(name) ?? [] }))
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// printing
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
|
|
399
|
+
function indent(text: string, spaces: number) {
|
|
400
|
+
const pad = ' '.repeat(spaces)
|
|
401
|
+
return text
|
|
402
|
+
.split('\n')
|
|
403
|
+
.map((line, i) => (i === 0 || line === '' ? line : pad + line))
|
|
404
|
+
.join('\n')
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
function printViewConfig(
|
|
408
|
+
componentName: string,
|
|
409
|
+
attributes: Attribute[],
|
|
410
|
+
events: EventProp[]
|
|
411
|
+
): string {
|
|
412
|
+
const lines: string[] = [` uiViewClassName: ${JSON.stringify(componentName)}`]
|
|
413
|
+
|
|
414
|
+
const bubbling = events.filter((e) => e.bubbling)
|
|
415
|
+
if (bubbling.length) {
|
|
416
|
+
lines.push(
|
|
417
|
+
` bubblingEventTypes: {\n${bubbling
|
|
418
|
+
.map(
|
|
419
|
+
(e) =>
|
|
420
|
+
` ${e.topName}: {\n phasedRegistrationNames: {\n captured: ${JSON.stringify(
|
|
421
|
+
`${e.name}Capture`
|
|
422
|
+
)},\n bubbled: ${JSON.stringify(e.name)}\n }\n }`
|
|
423
|
+
)
|
|
424
|
+
.join(',\n')}\n }`
|
|
425
|
+
)
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
const direct = events.filter((e) => !e.bubbling)
|
|
429
|
+
if (direct.length) {
|
|
430
|
+
lines.push(
|
|
431
|
+
` directEventTypes: {\n${direct
|
|
432
|
+
.map(
|
|
433
|
+
(e) =>
|
|
434
|
+
` ${e.topName}: {\n registrationName: ${JSON.stringify(e.name)}\n }`
|
|
435
|
+
)
|
|
436
|
+
.join(',\n')}\n }`
|
|
437
|
+
)
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const attrLines = attributes.map((a) => ` ${a.name}: ${indent(a.value, 4)}`)
|
|
441
|
+
if (events.length) {
|
|
442
|
+
attrLines.push(
|
|
443
|
+
` ...ConditionallyIgnoredEventHandlers({\n${events
|
|
444
|
+
.map((e) => ` ${e.name}: true`)
|
|
445
|
+
.join(',\n')}\n })`
|
|
446
|
+
)
|
|
447
|
+
}
|
|
448
|
+
lines.push(
|
|
449
|
+
attrLines.length ? ` validAttributes: {\n${attrLines.join(',\n')}\n }` : ' validAttributes: {}'
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
return `{\n${lines.join(',\n')}\n}`
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function printCommands(commands: Command[]): string {
|
|
456
|
+
return `export const Commands = {\n${commands
|
|
457
|
+
.map(
|
|
458
|
+
(c) =>
|
|
459
|
+
` ${c.name}(ref${c.params.map((p) => `, ${p}`).join('')}) {\n dispatchCommand(ref, ${JSON.stringify(
|
|
460
|
+
c.name
|
|
461
|
+
)}, [${c.params.join(', ')}]);\n }`
|
|
462
|
+
)
|
|
463
|
+
.join(',\n')}\n};`
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
const FILE_HEADER = `/**
|
|
467
|
+
* This code was generated by [react-native-codegen](https://www.npmjs.com/package/react-native-codegen).
|
|
468
|
+
*
|
|
469
|
+
* Do not edit this file as changes may cause incorrect behavior and will be lost
|
|
470
|
+
* once the code is regenerated.
|
|
471
|
+
*
|
|
472
|
+
* @flow
|
|
473
|
+
*
|
|
474
|
+
* @generated by codegen project: GenerateViewConfigJs.js
|
|
475
|
+
*/
|
|
476
|
+
|
|
477
|
+
'use strict';
|
|
478
|
+
`
|
|
479
|
+
|
|
480
|
+
// ---------------------------------------------------------------------------
|
|
481
|
+
// entry
|
|
482
|
+
// ---------------------------------------------------------------------------
|
|
483
|
+
|
|
484
|
+
export function parseSpec(code: string, filename: string, projectRoot?: string): any {
|
|
485
|
+
const isTS = /\.[cm]?tsx?$/.test(filename)
|
|
486
|
+
if (isTS) {
|
|
487
|
+
const result = parseSync(filename, code, {
|
|
488
|
+
lang: filename.endsWith('x') ? 'tsx' : 'ts',
|
|
489
|
+
})
|
|
490
|
+
if (result.errors?.length) return null
|
|
491
|
+
return result.program
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const hermes = getHermesParser(projectRoot)
|
|
495
|
+
if (!hermes) return null
|
|
496
|
+
try {
|
|
497
|
+
// `babel: false` keeps this on hermes' own ESTree output, which is what
|
|
498
|
+
// makes this path babel-free.
|
|
499
|
+
return hermes.parse(code, { babel: false, flow: 'all', sourceFilename: filename }) as any
|
|
500
|
+
} catch {
|
|
501
|
+
return null
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Generate the replacement for a spec's default export, plus the `Commands`
|
|
507
|
+
* export when the spec declares one. Returns null when the file is not a spec
|
|
508
|
+
* this can handle, so the caller can leave it alone.
|
|
509
|
+
*/
|
|
510
|
+
export function generateViewConfig(
|
|
511
|
+
program: any
|
|
512
|
+
): { imports: string[]; body: string } | null {
|
|
513
|
+
const body = program?.body
|
|
514
|
+
if (!body) return null
|
|
515
|
+
|
|
516
|
+
const call = findComponentCall(body)
|
|
517
|
+
if (!call) return null
|
|
518
|
+
|
|
519
|
+
const componentNameArg = call.arguments?.[0]
|
|
520
|
+
if (typeof componentNameArg?.value !== 'string') return null
|
|
521
|
+
const componentName = componentNameArg.value
|
|
522
|
+
|
|
523
|
+
const options = call.arguments?.[1]
|
|
524
|
+
const paperComponentName = readStringOption(options, 'paperComponentName')
|
|
525
|
+
const paperComponentNameDeprecated = readStringOption(
|
|
526
|
+
options,
|
|
527
|
+
'paperComponentNameDeprecated'
|
|
528
|
+
)
|
|
529
|
+
|
|
530
|
+
const propsTypeName = readTypeName(
|
|
531
|
+
(call.typeArguments ?? call.typeParameters)?.params?.[0]?.typeName ??
|
|
532
|
+
(call.typeArguments ?? call.typeParameters)?.params?.[0]?.id
|
|
533
|
+
)
|
|
534
|
+
if (!propsTypeName) return null
|
|
535
|
+
|
|
536
|
+
const decls = collectDeclarations(body)
|
|
537
|
+
const members = flattenMembers(propsTypeName, decls)
|
|
538
|
+
|
|
539
|
+
const attributes: Attribute[] = []
|
|
540
|
+
const events: EventProp[] = []
|
|
541
|
+
for (const m of members) {
|
|
542
|
+
const event = eventOf(m.name, m.type)
|
|
543
|
+
if (event) {
|
|
544
|
+
events.push(event)
|
|
545
|
+
continue
|
|
546
|
+
}
|
|
547
|
+
attributes.push({ name: m.name, value: attributeValue(m.type) })
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const commandsCall = findCommandsCall(body)
|
|
551
|
+
const commands = commandsCall ? readCommands(commandsCall, decls) : []
|
|
552
|
+
|
|
553
|
+
const imports = new Set([IMPORT_REGISTRY])
|
|
554
|
+
if (events.length) imports.add(IMPORT_IGNORE)
|
|
555
|
+
if (commands.length) imports.add(IMPORT_DISPATCH)
|
|
556
|
+
if (paperComponentNameDeprecated) imports.add(IMPORT_UIMANAGER)
|
|
557
|
+
|
|
558
|
+
const nativeName = paperComponentName ?? componentName
|
|
559
|
+
const parts: string[] = [`let nativeComponentName = '${nativeName}';`]
|
|
560
|
+
|
|
561
|
+
if (paperComponentNameDeprecated) {
|
|
562
|
+
parts.push(
|
|
563
|
+
`if (UIManager.hasViewManagerConfig('${componentName}')) {\n` +
|
|
564
|
+
` nativeComponentName = '${componentName}';\n` +
|
|
565
|
+
`} else if (UIManager.hasViewManagerConfig('${paperComponentNameDeprecated}')) {\n` +
|
|
566
|
+
` nativeComponentName = '${paperComponentNameDeprecated}';\n` +
|
|
567
|
+
`} else {\n` +
|
|
568
|
+
` throw new Error('Failed to find native component for either "${componentName}" or "${paperComponentNameDeprecated}"');\n` +
|
|
569
|
+
`}`
|
|
570
|
+
)
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
parts.push(
|
|
574
|
+
`export const __INTERNAL_VIEW_CONFIG = ${printViewConfig(nativeName, attributes, events)};`
|
|
575
|
+
)
|
|
576
|
+
parts.push(
|
|
577
|
+
'export default NativeComponentRegistry.get(nativeComponentName, () => __INTERNAL_VIEW_CONFIG);'
|
|
578
|
+
)
|
|
579
|
+
if (commands.length) parts.push(printCommands(commands))
|
|
580
|
+
|
|
581
|
+
return { imports: [...imports].sort(), body: parts.join('\n') }
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/** the full generated block, matching the layout codegen's file template emits */
|
|
585
|
+
export function renderViewConfigModule(result: { imports: string[]; body: string }) {
|
|
586
|
+
return `\n${FILE_HEADER}\n${result.imports.join('\n')}\n\n${result.body}\n`
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export function getLibraryName(filename: string): string {
|
|
590
|
+
const base = basename(filename)
|
|
591
|
+
const replaced = base.replace(/NativeComponent\.[cm]?[jt]sx?$/, '')
|
|
592
|
+
return replaced === base ? base.replace(/\.[cm]?[jt]sx?$/, '') : replaced
|
|
593
|
+
}
|