expo-modules-macros 0.25.0 → 0.27.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/AGENTS.md +8 -8
- package/README.md +40 -3
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Package.swift +10 -4
- package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/CLI.swift +45 -2
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/ExpandReport.swift +120 -0
- package/apple/Sources/ExpoModulesCLI/Commands/Expand/RunExpand.swift +183 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ExportedSurface.swift +3 -2
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ResolveRefs.swift +9 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/SurfaceVisitor.swift +10 -3
- package/apple/Sources/ExpoModulesCLI/Commands/ScanExports/TupleTypeAliases.swift +126 -0
- package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/TypeNode.swift +47 -4
- package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/SourceScan.swift +8 -2
- package/apple/Sources/ExpoModulesMacros/Expand/MacroExpander.swift +484 -0
- package/apple/Sources/ExpoModulesMacros/{EventMacro.swift → Macros/EventMacro.swift} +23 -10
- package/apple/Sources/ExpoModulesMacros/{ExpoModuleMacro.swift → Macros/ExpoModuleMacro.swift} +13 -1
- package/apple/Sources/ExpoModulesMacros/{JSMacro.swift → Macros/JSMacro.swift} +71 -54
- package/apple/Sources/ExpoModulesMacros/{RecordMacro.swift → Macros/RecordMacro.swift} +18 -5
- package/apple/Sources/ExpoModulesMacros/{SharedObjectMacro.swift → Macros/SharedObjectMacro.swift} +13 -1
- package/apple/Sources/ExpoModulesMacros/{UnionMacro.swift → Macros/UnionMacro.swift} +16 -5
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +9 -14
- package/apple/Sources/ExpoModulesMacros/ProvidedMacros.swift +31 -0
- package/apple/Sources/ExpoModulesMacros/{ClosureArguments.swift → Support/ClosureArguments.swift} +30 -4
- package/apple/Sources/ExpoModulesMacros/{DecorateModuleBuilder.swift → Support/DecorateModuleBuilder.swift} +5 -1
- package/apple/Sources/ExpoModulesMacros/{MacroHelpers.swift → Support/MacroHelpers.swift} +11 -4
- package/apple/Sources/ExpoModulesMacros/Support/TupleTypes.swift +272 -0
- package/apple/Sources/ExpoModulesMacros/Support/TypeAliases.swift +75 -0
- package/apple/Sources/ExpoModulesMacros/{TypeConformanceAssertion.swift → Support/TypeConformanceAssertion.swift} +104 -41
- package/build/index.d.ts +31 -1
- package/build/index.js +34 -0
- package/build/types.d.ts +77 -2
- package/build/types.js +4 -3
- package/package.json +5 -5
- /package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/Availability.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner/Exports → ExpoModulesCLI/Commands/ScanExports}/ScanExports.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner/Modules → ExpoModulesCLI/Commands/ScanModules}/ScanModules.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/Detection.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/DetectionVisitor.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/ScanBuildConfiguration.swift +0 -0
- /package/apple/Sources/{ExpoModulesScanner → ExpoModulesCLI}/Core/StandardStreams.swift +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ExpoModulesOptimizedMacro.swift → Macros/ExpoModulesOptimizedMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ExpoViewMacro.swift → Macros/ExpoViewMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{ViewPropsMacro.swift → Macros/ViewPropsMacro.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{FreeFormTypes.swift → Support/FreeFormTypes.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{JSConstructor.swift → Support/JSConstructor.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{JSNameCollisions.swift → Support/JSNameCollisions.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{OptimizedFunctionHelpers.swift → Support/OptimizedFunctionHelpers.swift} +0 -0
- /package/apple/Sources/ExpoModulesMacros/{Receiver.swift → Support/Receiver.swift} +0 -0
|
@@ -37,21 +37,43 @@ public struct JSMacro: PeerMacro {
|
|
|
37
37
|
diagnoseConcurrentOption(of: node, on: declaration, in: context)
|
|
38
38
|
diagnoseAsyncVariantOption(of: node, on: declaration, in: context)
|
|
39
39
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
decodableTypes: member.decodableTypes,
|
|
44
|
-
encodableType: member.encodableType,
|
|
45
|
-
callbackArgumentTypes: member.callbackArgumentTypes,
|
|
46
|
-
isStatic: member.isStatic
|
|
47
|
-
)
|
|
48
|
-
else {
|
|
40
|
+
// In an `@ExpoModule` or `@SharedObject` class, the class macro emits the assertion instead: it sees
|
|
41
|
+
// the type aliases declared in the class body, which this macro can't (see `TupleTypeAliases`).
|
|
42
|
+
guard !isInBindingClass(context), let assertion = jsConformanceAssertion(for: declaration) else {
|
|
49
43
|
return []
|
|
50
44
|
}
|
|
51
45
|
return [assertion]
|
|
52
46
|
}
|
|
53
47
|
}
|
|
54
48
|
|
|
49
|
+
/// The conformance-assertion peer for a `@JS` member, or `nil` when the member has no type to assert.
|
|
50
|
+
private func jsConformanceAssertion(for declaration: some DeclSyntaxProtocol) -> DeclSyntax? {
|
|
51
|
+
guard let member = boundaryMember(of: declaration) else {
|
|
52
|
+
return nil
|
|
53
|
+
}
|
|
54
|
+
return directionalConformanceAssertion(name: member.name, types: member.types, isStatic: member.isStatic)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/// The conformance assertion of a `@JS` member, for an `@ExpoModule` or `@SharedObject` class to fold
|
|
58
|
+
/// into its `_assertTypesConformance()` (see `classConformanceAssertions`), or `nil` when the member has
|
|
59
|
+
/// no type to assert.
|
|
60
|
+
internal func jsConformanceAssertionBody(for declaration: some DeclSyntaxProtocol) -> MemberConformanceAssertion? {
|
|
61
|
+
guard let member = boundaryMember(of: declaration) else {
|
|
62
|
+
return nil
|
|
63
|
+
}
|
|
64
|
+
return memberConformanceAssertion(name: member.name, types: member.types)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// True when the member is declared directly in an `@ExpoModule` or `@SharedObject` class, whose macro
|
|
68
|
+
/// binds it.
|
|
69
|
+
private func isInBindingClass(_ context: some MacroExpansionContext) -> Bool {
|
|
70
|
+
guard let classDecl = context.lexicalContext.first?.as(ClassDeclSyntax.self) else {
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
return classDecl.attributes.firstAttribute(named: "ExpoModule") != nil
|
|
74
|
+
|| classDecl.attributes.firstAttribute(named: "SharedObject") != nil
|
|
75
|
+
}
|
|
76
|
+
|
|
55
77
|
/// Emits the diagnostic for `@JS(.concurrent)` on a member that can't take it. The option maps to
|
|
56
78
|
/// Swift's `@concurrent`, which requires an `async` function: a synchronous member has nowhere to
|
|
57
79
|
/// suspend, and a property or initializer can't be async at all. Diagnosing here points at the
|
|
@@ -323,28 +345,24 @@ private struct JSFixItMessage: FixItMessage {
|
|
|
323
345
|
}
|
|
324
346
|
}
|
|
325
347
|
|
|
326
|
-
/// What an assertion
|
|
327
|
-
///
|
|
328
|
-
/// type-level. Arguments (and a settable property's incoming value) are decoded;
|
|
329
|
-
/// property's outgoing value) are encoded, so each is asserted against the
|
|
348
|
+
/// What an assertion needs about a `@JS` member: a name (which names the helper), the boundary types in
|
|
349
|
+
/// signature order, each with the protocol for the direction it travels and its argument label, and
|
|
350
|
+
/// whether the member is type-level. Arguments (and a settable property's incoming value) are decoded;
|
|
351
|
+
/// return values (and a property's outgoing value) are encoded, so each is asserted against the
|
|
352
|
+
/// protocol for its direction.
|
|
330
353
|
private struct BoundaryMember {
|
|
331
354
|
let name: String
|
|
332
|
-
///
|
|
333
|
-
|
|
334
|
-
let decodableTypes: [String]
|
|
335
|
-
/// The single type encoded to JS: a function's return type, or a property's value type on read;
|
|
336
|
-
/// `nil` when the member produces nothing JS-visible (a `Void` function).
|
|
337
|
-
let encodableType: String?
|
|
338
|
-
/// The parameter types of closure arguments, encoded to JS when native code calls the closure.
|
|
339
|
-
var callbackArgumentTypes: [String] = []
|
|
355
|
+
/// The types crossing the boundary, in signature order.
|
|
356
|
+
let types: [AssertedType]
|
|
340
357
|
/// True for `static`/`class` members, so the peer is emitted in the same metatype context.
|
|
341
358
|
let isStatic: Bool
|
|
342
359
|
}
|
|
343
360
|
|
|
344
|
-
/// Reads the boundary member off a `@JS` declaration,
|
|
345
|
-
///
|
|
346
|
-
/// an initializer contributes its parameter types; a property
|
|
347
|
-
///
|
|
361
|
+
/// Reads the boundary member off a `@JS` declaration, with each type's conversion direction. A function
|
|
362
|
+
/// contributes its parameter types (decodable) under their argument labels and its return type when
|
|
363
|
+
/// non-Void (encodable) under `returning`; an initializer contributes its parameter types; a property
|
|
364
|
+
/// contributes its value type, unlabeled, as decodable when settable (the setter) and as encodable (the
|
|
365
|
+
/// getter). Composed types (`[Int]`, `String?`,
|
|
348
366
|
/// …) are kept verbatim, their conditional conformances transitively constraining the elements. A type
|
|
349
367
|
/// that mentions `Any` is left out: it converts through the free-form methods, or is diagnosed by
|
|
350
368
|
/// `diagnoseFreeFormTypes`. Returns `nil` for declaration kinds `@JS` doesn't read types from, a
|
|
@@ -352,30 +370,28 @@ private struct BoundaryMember {
|
|
|
352
370
|
/// mentions `Any`.
|
|
353
371
|
private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> BoundaryMember? {
|
|
354
372
|
if let funcDecl = declaration.as(FunctionDeclSyntax.self) {
|
|
355
|
-
|
|
356
|
-
let returnType = funcDecl.signature.returnClause?.type
|
|
357
|
-
|
|
358
|
-
|
|
373
|
+
var types = argumentBoundaryTypes(of: funcDecl.signature.parameterClause.parameters)
|
|
374
|
+
if let returnType = funcDecl.signature.returnClause?.type,
|
|
375
|
+
!isVoidType(returnType) && !containsFunctionType(returnType) && !mentionsFreeFormAny(returnType)
|
|
376
|
+
{
|
|
377
|
+
types.append(
|
|
378
|
+
AssertedType(
|
|
379
|
+
label: "returning",
|
|
380
|
+
type: returnType.trimmedDescription,
|
|
381
|
+
constraint: javaScriptEncodableProtocolName
|
|
382
|
+
)
|
|
383
|
+
)
|
|
359
384
|
}
|
|
360
|
-
return BoundaryMember(
|
|
361
|
-
name: funcDecl.name.text,
|
|
362
|
-
decodableTypes: argumentTypes.decodable,
|
|
363
|
-
encodableType: encodableType,
|
|
364
|
-
callbackArgumentTypes: argumentTypes.callbackArguments,
|
|
365
|
-
isStatic: isTypeLevel(funcDecl.modifiers)
|
|
366
|
-
)
|
|
385
|
+
return BoundaryMember(name: funcDecl.name.text, types: types, isStatic: isTypeLevel(funcDecl.modifiers))
|
|
367
386
|
}
|
|
368
387
|
|
|
369
388
|
if let initDecl = declaration.as(InitializerDeclSyntax.self) {
|
|
370
389
|
// A constructor decodes its arguments like a function does, and the instance it returns isn't
|
|
371
390
|
// encoded. The name is escaped because it also names the assertion's nested helper, and `init` is
|
|
372
391
|
// a keyword.
|
|
373
|
-
let argumentTypes = argumentBoundaryTypes(of: initDecl.signature.parameterClause.parameters)
|
|
374
392
|
return BoundaryMember(
|
|
375
393
|
name: "`init`",
|
|
376
|
-
|
|
377
|
-
encodableType: nil,
|
|
378
|
-
callbackArgumentTypes: argumentTypes.callbackArguments,
|
|
394
|
+
types: argumentBoundaryTypes(of: initDecl.signature.parameterClause.parameters),
|
|
379
395
|
isStatic: false
|
|
380
396
|
)
|
|
381
397
|
}
|
|
@@ -395,10 +411,11 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
395
411
|
let typeText = type.trimmedDescription
|
|
396
412
|
let isVar = varDecl.bindingSpecifier.tokenKind == .keyword(.var)
|
|
397
413
|
let isSettable = isVar && !isLazy(varDecl.modifiers) && bindingIsSettable(binding)
|
|
414
|
+
let decodable = AssertedType(label: nil, type: typeText, constraint: javaScriptDecodableProtocolName)
|
|
415
|
+
let encodable = AssertedType(label: nil, type: typeText, constraint: javaScriptEncodableProtocolName)
|
|
398
416
|
return BoundaryMember(
|
|
399
417
|
name: identifier.identifier.text,
|
|
400
|
-
|
|
401
|
-
encodableType: typeText,
|
|
418
|
+
types: (isSettable ? [decodable] : []) + [encodable],
|
|
402
419
|
isStatic: isTypeLevel(varDecl.modifiers)
|
|
403
420
|
)
|
|
404
421
|
}
|
|
@@ -406,18 +423,16 @@ private func boundaryMember(of declaration: some DeclSyntaxProtocol) -> Boundary
|
|
|
406
423
|
return nil
|
|
407
424
|
}
|
|
408
425
|
|
|
409
|
-
/// The boundary types of a function's or constructor's arguments
|
|
410
|
-
/// a closure argument instead contributes its parameter types (encoded
|
|
411
|
-
/// its return type (decoded).
|
|
412
|
-
private func argumentBoundaryTypes(
|
|
413
|
-
of parameters: FunctionParameterListSyntax
|
|
414
|
-
) -> (decodable: [String], callbackArguments: [String]) {
|
|
426
|
+
/// The boundary types of a function's or constructor's arguments, under their argument labels: an
|
|
427
|
+
/// argument is decoded from JS, and a closure argument instead contributes its parameter types (encoded
|
|
428
|
+
/// when native code calls it) and its return type (decoded).
|
|
429
|
+
private func argumentBoundaryTypes(of parameters: FunctionParameterListSyntax) -> [AssertedType] {
|
|
415
430
|
// A closure argument moves data the other way: JS receives its parameters and returns its result.
|
|
416
431
|
// A closure in any other position is diagnosed by `diagnoseClosureTypes` and left out here, since
|
|
417
432
|
// its metatype isn't a valid expression in the assertion.
|
|
418
|
-
var
|
|
419
|
-
var callbackArgumentTypes: [String] = []
|
|
433
|
+
var types: [AssertedType] = []
|
|
420
434
|
for parameter in parameters {
|
|
435
|
+
let label = parameter.firstName.tokenKind == .wildcard ? nil : parameter.firstName.text
|
|
421
436
|
// The specifier isn't part of the asserted type, and a parameter with one the binding can't honor
|
|
422
437
|
// is reported by `diagnoseParameterSpecifiers` and not asserted.
|
|
423
438
|
let specified = SpecifiedType(parameter.type)
|
|
@@ -426,23 +441,25 @@ private func argumentBoundaryTypes(
|
|
|
426
441
|
}
|
|
427
442
|
guard let closure = JSClosureType(specified.type) else {
|
|
428
443
|
if !containsFunctionType(specified.type) && !mentionsFreeFormAny(specified.type) {
|
|
429
|
-
|
|
444
|
+
let type = specified.type.trimmedDescription
|
|
445
|
+
types.append(AssertedType(label: label, type: type, constraint: javaScriptDecodableProtocolName))
|
|
430
446
|
}
|
|
431
447
|
continue
|
|
432
448
|
}
|
|
433
449
|
// A closure type that mentions `Any` is reported by `diagnoseClosureTypes` instead.
|
|
434
450
|
for parameter in closure.parameters
|
|
435
451
|
where !containsFunctionType(parameter.type) && !mentionsFreeFormAny(parameter.type) {
|
|
436
|
-
|
|
452
|
+
let type = parameter.type.trimmedDescription
|
|
453
|
+
types.append(AssertedType(label: label, type: type, constraint: javaScriptEncodableProtocolName))
|
|
437
454
|
}
|
|
438
455
|
let closureReturnType = closure.functionType.returnClause.type
|
|
439
456
|
if let returnType = closure.returnType,
|
|
440
457
|
!containsFunctionType(closureReturnType) && !mentionsFreeFormAny(closureReturnType)
|
|
441
458
|
{
|
|
442
|
-
|
|
459
|
+
types.append(AssertedType(label: label, type: returnType, constraint: javaScriptDecodableProtocolName))
|
|
443
460
|
}
|
|
444
461
|
}
|
|
445
|
-
return
|
|
462
|
+
return types
|
|
446
463
|
}
|
|
447
464
|
|
|
448
465
|
/// True when a return clause is written as `Void` / `()` — nothing crosses the boundary, so it needs
|
|
@@ -423,10 +423,17 @@ private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
|
423
423
|
let entry = "entry\(index)"
|
|
424
424
|
let exprType = expressionType(property.type)
|
|
425
425
|
let isBareAny = property.freeForm?.isBareAny == true
|
|
426
|
-
let cast
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
426
|
+
let cast: String
|
|
427
|
+
if isBareAny {
|
|
428
|
+
cast = entry
|
|
429
|
+
} else if tupleConversion(of: property.type) != nil {
|
|
430
|
+
// A type that holds a tuple has no dynamic type, so the dictionary value must already be of the
|
|
431
|
+
// property's type.
|
|
432
|
+
cast =
|
|
433
|
+
"try { () throws -> \(exprType) in guard let value = \(entry) as? \(exprType) else { throw Conversions.ConvertingException<\(exprType)>(\(entry)) }; return value }()"
|
|
434
|
+
} else {
|
|
435
|
+
cast = "try \(exprType).getDynamicType().cast(\(entry), appContext: appContext) as! \(exprType)"
|
|
436
|
+
}
|
|
430
437
|
lines.append(" let \(entry) = dictionary[\"\(property.key)\"]")
|
|
431
438
|
if property.isRequired {
|
|
432
439
|
lines.append(" guard let \(entry) else {")
|
|
@@ -544,7 +551,13 @@ private func objectWriteLines(properties: [RecordProperty], of record: String, c
|
|
|
544
551
|
encode = "try JavaScriptValue.encodeAny(\(value) as Any, in: runtime)"
|
|
545
552
|
case nil:
|
|
546
553
|
let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
|
|
547
|
-
|
|
554
|
+
// A type that holds a tuple has no `encode(_:into:in:)`, so the tuple methods encode it into the
|
|
555
|
+
// property's slot.
|
|
556
|
+
if let tuple = tupleConversion(of: property.type) {
|
|
557
|
+
return
|
|
558
|
+
" try object.setProperty(\(propName)) { slot, runtime in try \(tuple.encodeCall(of: value, into: "slot")) }"
|
|
559
|
+
}
|
|
560
|
+
return " try object.setProperty(\(propName), encoding: \(value))"
|
|
548
561
|
}
|
|
549
562
|
let propName = propNameArgument(for: property, at: index, cached: cachesPropNames)
|
|
550
563
|
return " object.setProperty(\(propName), value: \(encode))"
|
package/apple/Sources/ExpoModulesMacros/{SharedObjectMacro.swift → Macros/SharedObjectMacro.swift}
RENAMED
|
@@ -50,9 +50,15 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
50
50
|
var staticFunctions: [JSFunction] = []
|
|
51
51
|
var staticProperties: [JSProperty] = []
|
|
52
52
|
var constructor: JSConstructor?
|
|
53
|
+
var assertions: [MemberConformanceAssertion] = []
|
|
54
|
+
let aliases = TupleTypeAliases(in: classDecl.memberBlock)
|
|
53
55
|
|
|
54
56
|
for member in classDecl.memberBlock.members {
|
|
55
|
-
|
|
57
|
+
// The types of a `@JS` member are read with the class's tuple aliases resolved.
|
|
58
|
+
let decl = aliases.resolving(member.decl)
|
|
59
|
+
if memberHasJSAttribute(decl), let assertion = jsConformanceAssertionBody(for: decl) {
|
|
60
|
+
assertions.append(assertion)
|
|
61
|
+
}
|
|
56
62
|
|
|
57
63
|
if let initDecl = decl.as(InitializerDeclSyntax.self),
|
|
58
64
|
initDecl.attributes.firstAttribute(named: "JS") != nil
|
|
@@ -129,6 +135,12 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
129
135
|
emitted.append(constructor.buildConstructor(typeName: typeName))
|
|
130
136
|
}
|
|
131
137
|
|
|
138
|
+
// The conformance assertions of the `@JS` members, which `@JS` leaves to this macro (see
|
|
139
|
+
// `jsConformanceAssertionBody`).
|
|
140
|
+
if let assertion = classConformanceAssertions(assertions) {
|
|
141
|
+
emitted.append(assertion)
|
|
142
|
+
}
|
|
143
|
+
|
|
132
144
|
return emitted
|
|
133
145
|
}
|
|
134
146
|
}
|
|
@@ -270,9 +270,9 @@ private func validatedUnion(of declaration: some DeclGroupSyntax) throws -> Unio
|
|
|
270
270
|
private func decodeMethod(union: UnionType) -> DeclSyntax {
|
|
271
271
|
var lines: [String] = [" let kind = JavaScriptValueKinds(of: value)"]
|
|
272
272
|
for unionCase in union.cases {
|
|
273
|
-
let
|
|
273
|
+
let conversion = payloadConversion(unionCase)
|
|
274
274
|
lines.append(
|
|
275
|
-
" if \(
|
|
275
|
+
" if \(conversion.decodableKindsExpression).contains(kind), let payload = try? \(conversion.decodeExpression(of: "value")) {"
|
|
276
276
|
)
|
|
277
277
|
lines.append(" return \(unionCase.construction)")
|
|
278
278
|
lines.append(" }")
|
|
@@ -293,7 +293,7 @@ private func decodeMethod(union: UnionType) -> DeclSyntax {
|
|
|
293
293
|
/// `decodableKinds`: the union of the payloads' kinds, so a union nested in another union, or wrapped
|
|
294
294
|
/// in an optional, is skipped as precisely as its payloads are.
|
|
295
295
|
private func decodableKindsProperty(union: UnionType) -> DeclSyntax {
|
|
296
|
-
let kinds = union.cases.map {
|
|
296
|
+
let kinds = union.cases.map { payloadConversion($0).decodableKindsExpression }
|
|
297
297
|
// An option set's array literal is the union of its elements, which reads like the case list. The
|
|
298
298
|
// optimizer folds it to a constant after specialization, the same as a chain of `union(_:)` calls.
|
|
299
299
|
let expression = kinds.count == 1 ? kinds[0] : "[\(kinds.joined(separator: ", "))]"
|
|
@@ -311,7 +311,7 @@ private func encodeMethod(union: UnionType) -> DeclSyntax {
|
|
|
311
311
|
var lines: [String] = [" switch value {"]
|
|
312
312
|
for unionCase in union.cases {
|
|
313
313
|
lines.append(" case \(unionCase.pattern):")
|
|
314
|
-
lines.append(" return try \(
|
|
314
|
+
lines.append(" return try \(payloadConversion(unionCase).encodeExpression(of: "payload"))")
|
|
315
315
|
}
|
|
316
316
|
lines.append(" }")
|
|
317
317
|
let body = lines.joined(separator: "\n")
|
|
@@ -331,7 +331,12 @@ private func encodeIntoSlotMethod(union: UnionType) -> DeclSyntax {
|
|
|
331
331
|
var lines: [String] = [" switch value {"]
|
|
332
332
|
for unionCase in union.cases {
|
|
333
333
|
lines.append(" case \(unionCase.pattern):")
|
|
334
|
-
|
|
334
|
+
if let tuple = tupleConversion(of: unionCase.payloadType) {
|
|
335
|
+
// A payload that holds a tuple has no `encode(_:into:in:)`, so the tuple methods encode it into the slot.
|
|
336
|
+
lines.append(" try \(tuple.encodeCall(of: "payload", into: "slot"))")
|
|
337
|
+
} else {
|
|
338
|
+
lines.append(" try \(expressionType(unionCase.payloadType)).encode(payload, into: slot, in: runtime)")
|
|
339
|
+
}
|
|
335
340
|
}
|
|
336
341
|
lines.append(" }")
|
|
337
342
|
let body = lines.joined(separator: "\n")
|
|
@@ -439,3 +444,9 @@ private struct UnionFixItMessage: FixItMessage {
|
|
|
439
444
|
self.fixItID = MessageID(domain: "ExpoModulesMacros", id: id)
|
|
440
445
|
}
|
|
441
446
|
}
|
|
447
|
+
|
|
448
|
+
/// How a case's payload converts: through the tuple methods when it holds a tuple (see
|
|
449
|
+
/// `TupleConversion`), and through its own `decode` / `encode` otherwise.
|
|
450
|
+
private func payloadConversion(_ unionCase: UnionCase) -> TupleConversion {
|
|
451
|
+
return tupleConversion(of: unionCase.payloadType) ?? .conforming(type: unionCase.payloadType)
|
|
452
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import ExpoModulesCLI
|
|
2
2
|
import SwiftCompilerPlugin
|
|
3
3
|
import SwiftSyntaxMacros
|
|
4
4
|
|
|
@@ -11,23 +11,14 @@ import CRT
|
|
|
11
11
|
#endif
|
|
12
12
|
|
|
13
13
|
struct ExpoModulesMacrosPlugin: CompilerPlugin {
|
|
14
|
-
let providingMacros: [Macro.Type] =
|
|
15
|
-
OptimizedFunctionAttachedMacro.self,
|
|
16
|
-
JSMacro.self,
|
|
17
|
-
EventMacro.self,
|
|
18
|
-
ExpoModuleMacro.self,
|
|
19
|
-
SharedObjectMacro.self,
|
|
20
|
-
RecordMacro.self,
|
|
21
|
-
UnionMacro.self,
|
|
22
|
-
ViewPropsMacro.self,
|
|
23
|
-
ExpoViewMacro.self,
|
|
24
|
-
]
|
|
14
|
+
let providingMacros: [Macro.Type] = ProvidedMacro.all.map(\.type)
|
|
25
15
|
}
|
|
26
16
|
|
|
27
17
|
/// The executable doubles as the scanner CLI. The compiler always launches a plugin executable
|
|
28
18
|
/// without arguments and speaks the plugin protocol over stdin, so any argument means a scanner
|
|
29
19
|
/// invocation (`ExpoModulesMacros scan-modules <path>...`); with none, this starts the plugin
|
|
30
|
-
/// server exactly as `@main` on the `CompilerPlugin` type would.
|
|
20
|
+
/// server exactly as `@main` on the `CompilerPlugin` type would. The CLI gets `expandMacros`, so
|
|
21
|
+
/// `expand` runs the macros of this executable.
|
|
31
22
|
@main
|
|
32
23
|
enum EntryPoint {
|
|
33
24
|
static func main() throws {
|
|
@@ -35,7 +26,11 @@ enum EntryPoint {
|
|
|
35
26
|
if arguments.isEmpty {
|
|
36
27
|
try ExpoModulesMacrosPlugin.main()
|
|
37
28
|
} else {
|
|
38
|
-
|
|
29
|
+
let expander = MacroExpander(
|
|
30
|
+
attributeNames: Set(ProvidedMacro.all.map(\.name)),
|
|
31
|
+
expand: expandMacros
|
|
32
|
+
)
|
|
33
|
+
exit(ScannerCLI.run(arguments: arguments, expander: expander))
|
|
39
34
|
}
|
|
40
35
|
}
|
|
41
36
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxMacros
|
|
3
|
+
|
|
4
|
+
/// A macro that the plugin provides, with the attribute name and the extension conformances of its
|
|
5
|
+
/// declaration in `expo-modules-core`.
|
|
6
|
+
struct ProvidedMacro {
|
|
7
|
+
let name: String
|
|
8
|
+
let type: Macro.Type
|
|
9
|
+
var conformances: [TypeSyntax] = []
|
|
10
|
+
|
|
11
|
+
/// Every macro of the plugin. The compiler gets the types from `providingMacros`, and `expand`
|
|
12
|
+
/// matches the attributes by name and passes the same conformances as the compiler, so both read
|
|
13
|
+
/// this list.
|
|
14
|
+
static var all: [ProvidedMacro] {
|
|
15
|
+
[
|
|
16
|
+
ProvidedMacro(name: "OptimizedFunction", type: OptimizedFunctionAttachedMacro.self),
|
|
17
|
+
ProvidedMacro(name: "JS", type: JSMacro.self),
|
|
18
|
+
ProvidedMacro(name: "Event", type: EventMacro.self),
|
|
19
|
+
ProvidedMacro(name: "ExpoModule", type: ExpoModuleMacro.self, conformances: ["AnyModule"]),
|
|
20
|
+
ProvidedMacro(name: "SharedObject", type: SharedObjectMacro.self),
|
|
21
|
+
ProvidedMacro(name: "Record", type: RecordMacro.self, conformances: ["Record"]),
|
|
22
|
+
ProvidedMacro(
|
|
23
|
+
name: "Union",
|
|
24
|
+
type: UnionMacro.self,
|
|
25
|
+
conformances: ["JavaScriptDecodable", "JavaScriptEncodable"]
|
|
26
|
+
),
|
|
27
|
+
ProvidedMacro(name: "ViewProps", type: ViewPropsMacro.self, conformances: ["AnyViewProps"]),
|
|
28
|
+
ProvidedMacro(name: "ExpoView", type: ExpoViewMacro.self),
|
|
29
|
+
]
|
|
30
|
+
}
|
|
31
|
+
}
|
package/apple/Sources/ExpoModulesMacros/{ClosureArguments.swift → Support/ClosureArguments.swift}
RENAMED
|
@@ -146,13 +146,20 @@ private func closureLiteralLines(_ closure: JSClosureType, callback: String) ->
|
|
|
146
146
|
// parameters instead would send non-`Sendable` values to the `@JavaScriptActor` closure, which
|
|
147
147
|
// Swift 6 rejects. The closure captures nothing, so it needs no allocation. Without parameters
|
|
148
148
|
// there is nothing to emplace, and `invokeAsync` takes the parameters variadically.
|
|
149
|
-
|
|
149
|
+
//
|
|
150
|
+
// A result type that holds a tuple can't be passed as `returning:`, so a sync closure returning one
|
|
151
|
+
// decodes it through a `decoding:` closure, which only the `emplacing` variant takes. With no
|
|
152
|
+
// parameters it emplaces nothing.
|
|
153
|
+
let returnTuple = closure.returnType.flatMap { tupleConversion(of: $0) }
|
|
154
|
+
let emplaces = !closure.isAsync && (!parameters.isEmpty || returnTuple != nil)
|
|
150
155
|
var arguments = parameters
|
|
151
156
|
if emplaces {
|
|
152
157
|
let values = parameters.count == 1 ? "p0" : "(" + parameters.joined(separator: ", ") + ")"
|
|
153
158
|
arguments = [values, "argumentCount: \(parameters.count)"]
|
|
154
159
|
}
|
|
155
|
-
if let
|
|
160
|
+
if let returnTuple {
|
|
161
|
+
arguments.append("decoding: \(elementClosure(returning: returnTuple.decodeExpression(of: "value")))")
|
|
162
|
+
} else if let returnType = closure.returnType {
|
|
156
163
|
arguments.append("returning: \(expressionType(returnType)).self")
|
|
157
164
|
}
|
|
158
165
|
let argumentList = arguments.joined(separator: ", ")
|
|
@@ -168,9 +175,16 @@ private func closureLiteralLines(_ closure: JSClosureType, callback: String) ->
|
|
|
168
175
|
|
|
169
176
|
if emplaces {
|
|
170
177
|
var lines = [header, " \(call) { _values, _arguments, _runtime in"]
|
|
171
|
-
for index in parameters.
|
|
178
|
+
for (index, parameter) in closure.parameters.enumerated() {
|
|
172
179
|
let value = parameters.count == 1 ? "_values" : "_values.\(index)"
|
|
173
|
-
|
|
180
|
+
// A parameter that holds a tuple has no `encode(_:into:in:)`, so the tuple methods encode it into the
|
|
181
|
+
// argument's slot.
|
|
182
|
+
if let tuple = tupleConversion(of: parameter.type) {
|
|
183
|
+
let encode = tuple.encodeCall(of: value, into: "slot", in: "_runtime")
|
|
184
|
+
lines.append(" try _arguments.emplace(at: \(index)) { slot in try \(encode) }")
|
|
185
|
+
} else {
|
|
186
|
+
lines.append(" try _arguments.emplace(\(value), at: \(index), in: _runtime)")
|
|
187
|
+
}
|
|
174
188
|
}
|
|
175
189
|
return lines + [" }", "}"]
|
|
176
190
|
}
|
|
@@ -336,6 +350,18 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
|
|
|
336
350
|
)
|
|
337
351
|
)
|
|
338
352
|
)
|
|
353
|
+
} else if closure.isAsync && tupleConversion(of: type) != nil {
|
|
354
|
+
// `invokeAsync` takes its arguments and result as `JavaScriptEncodable` / `JavaScriptDecodable`
|
|
355
|
+
// types, and a type that holds a tuple can't conform.
|
|
356
|
+
context.diagnose(
|
|
357
|
+
Diagnostic(
|
|
358
|
+
node: type,
|
|
359
|
+
message: ClosureDiagnosticMessage(
|
|
360
|
+
"An async @JS closure argument can't take or return a tuple. Use a @Record type, or a closure that isn't async.",
|
|
361
|
+
id: "js-closure-async-tuple"
|
|
362
|
+
)
|
|
363
|
+
)
|
|
364
|
+
)
|
|
339
365
|
}
|
|
340
366
|
}
|
|
341
367
|
|
|
@@ -499,13 +499,17 @@ private let resultSlotClosureParameters =
|
|
|
499
499
|
+ "result: borrowing JavaScriptValue.Slot, runtime: borrowing JavaScriptRuntime)"
|
|
500
500
|
|
|
501
501
|
/// The statement that encodes `valueExpression` of a boundary type into the result slot `result`. A type
|
|
502
|
-
/// that mentions `Any` has no `encode(_:into:in:)`, so its free-form encode result is moved into the slot
|
|
502
|
+
/// that mentions `Any` has no `encode(_:into:in:)`, so its free-form encode result is moved into the slot,
|
|
503
|
+
/// and a type that holds a tuple is encoded into the slot by the tuple methods.
|
|
503
504
|
private func encodeIntoSlotStatement(_ type: String, freeForm: FreeFormConversion?, of valueExpression: String)
|
|
504
505
|
-> String
|
|
505
506
|
{
|
|
506
507
|
if freeForm != nil {
|
|
507
508
|
return "result.emplace(try \(encodeCall(type, freeForm: freeForm, of: valueExpression)))"
|
|
508
509
|
}
|
|
510
|
+
if let tuple = tupleConversion(of: type) {
|
|
511
|
+
return "try \(tuple.encodeCall(of: valueExpression, into: "result"))"
|
|
512
|
+
}
|
|
509
513
|
return "try \(expressionType(type)).encode(\(valueExpression), into: result, in: runtime)"
|
|
510
514
|
}
|
|
511
515
|
|
|
@@ -493,23 +493,30 @@ internal func expressionType(_ type: String) -> String {
|
|
|
493
493
|
|
|
494
494
|
/// The decode expression for a boundary type read from `valueExpression` (a `JavaScriptUnownedValue`),
|
|
495
495
|
/// as it appears after `try`. A type that mentions `Any` can't conform to `JavaScriptDecodable`, so it
|
|
496
|
-
/// decodes through the `JavaScriptValue` free-form methods its `freeForm` conversion names
|
|
497
|
-
///
|
|
498
|
-
/// `T?`).
|
|
496
|
+
/// decodes through the `JavaScriptValue` free-form methods its `freeForm` conversion names, and a tuple
|
|
497
|
+
/// through the `JavaScriptValue` tuple methods (see `TupleConversion`); every other type decodes through
|
|
498
|
+
/// its own static `decode`, spelled in expression position (`T!` rewritten to `T?`).
|
|
499
499
|
internal func decodeCall(_ type: String, freeForm: FreeFormConversion?, from valueExpression: String) -> String {
|
|
500
500
|
if let freeForm {
|
|
501
501
|
return freeForm.decodeExpression(of: valueExpression, as: expressionType(type))
|
|
502
502
|
}
|
|
503
|
+
if let tuple = tupleConversion(of: type) {
|
|
504
|
+
return tuple.decodeExpression(of: valueExpression)
|
|
505
|
+
}
|
|
503
506
|
return "\(expressionType(type)).decode(\(valueExpression), in: runtime)"
|
|
504
507
|
}
|
|
505
508
|
|
|
506
509
|
/// The encode expression for the native value `valueExpression` of a boundary type, as it appears
|
|
507
510
|
/// after `try`. The counterpart of `decodeCall`: a type that mentions `Any` encodes through the
|
|
508
|
-
/// `JavaScriptValue` free-form methods,
|
|
511
|
+
/// `JavaScriptValue` free-form methods, a tuple through the `JavaScriptValue` tuple methods, and every
|
|
512
|
+
/// other type through its own static `encode`.
|
|
509
513
|
internal func encodeCall(_ type: String, freeForm: FreeFormConversion?, of valueExpression: String) -> String {
|
|
510
514
|
if let freeForm {
|
|
511
515
|
return freeForm.encodeExpression(of: valueExpression)
|
|
512
516
|
}
|
|
517
|
+
if let tuple = tupleConversion(of: type) {
|
|
518
|
+
return tuple.encodeExpression(of: valueExpression)
|
|
519
|
+
}
|
|
513
520
|
return "\(expressionType(type)).encode(\(valueExpression), in: runtime)"
|
|
514
521
|
}
|
|
515
522
|
|