@expo/expo-modules-macros-plugin 0.0.8 → 0.1.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/.github/workflows/publish.yml +116 -0
- package/.github/workflows/swift.yml +59 -0
- package/apple/ExpoModulesMacros-tool +0 -0
- package/apple/Package.swift +12 -4
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +244 -0
- package/apple/Sources/ExpoModulesMacros/JSMacro.swift +29 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +168 -0
- package/apple/Sources/ExpoModulesMacros/Plugin.swift +4 -0
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +523 -0
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +188 -0
- package/apple/build.js +129 -12
- package/package.json +1 -1
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import SwiftSyntax
|
|
2
|
+
import SwiftSyntaxBuilder
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
Member macro applied to a `SharedObject` subclass. Scans the class body for
|
|
7
|
+
declarations marked with `@JS` and synthesizes `_synthesizedClassDefinition()`,
|
|
8
|
+
returning a `ClassDefinition` ready to drop into a module's `Class { ... }` slot.
|
|
9
|
+
|
|
10
|
+
@SharedObject
|
|
11
|
+
final class Cache: SharedObject {
|
|
12
|
+
@JS
|
|
13
|
+
init(name: String) { self.name = name }
|
|
14
|
+
|
|
15
|
+
@JS
|
|
16
|
+
func get(_ key: String) -> String? { ... }
|
|
17
|
+
|
|
18
|
+
@JS
|
|
19
|
+
var size: Int { 42 }
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
The companion `@ExpoModule(classes: [Cache.self])` wires the resulting
|
|
23
|
+
definition into the module's exposed surface.
|
|
24
|
+
*/
|
|
25
|
+
public struct SharedObjectMacro: MemberMacro {
|
|
26
|
+
public static func expansion(
|
|
27
|
+
of node: AttributeSyntax,
|
|
28
|
+
providingMembersOf declaration: some DeclGroupSyntax,
|
|
29
|
+
conformingTo protocols: [TypeSyntax],
|
|
30
|
+
in context: some MacroExpansionContext
|
|
31
|
+
) throws -> [DeclSyntax] {
|
|
32
|
+
guard let classDecl = declaration.as(ClassDeclSyntax.self) else {
|
|
33
|
+
throw MacroExpansionErrorMessage("@SharedObject can only be applied to a class")
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
guard inheritsFromSharedObject(classDecl) else {
|
|
37
|
+
throw MacroExpansionErrorMessage(
|
|
38
|
+
"@SharedObject class must inherit from SharedObject. Add `: SharedObject` to the class declaration.")
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
let typeName = classDecl.name.text
|
|
42
|
+
let jsName = jsNameArgument(of: node) ?? typeName
|
|
43
|
+
|
|
44
|
+
var entries: [String] = []
|
|
45
|
+
var sawConstructor = false
|
|
46
|
+
|
|
47
|
+
for member in classDecl.memberBlock.members {
|
|
48
|
+
let decl = member.decl
|
|
49
|
+
|
|
50
|
+
if let initDecl = decl.as(InitializerDeclSyntax.self),
|
|
51
|
+
initDecl.attributes.firstAttribute(named: "JS") != nil {
|
|
52
|
+
if sawConstructor {
|
|
53
|
+
throw MacroExpansionErrorMessage(
|
|
54
|
+
"@SharedObject classes can have at most one @JS initializer; JavaScript classes have a single constructor.")
|
|
55
|
+
}
|
|
56
|
+
sawConstructor = true
|
|
57
|
+
entries.append(buildConstructorEntry(initDecl: initDecl, typeName: typeName))
|
|
58
|
+
continue
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if let funcDecl = decl.as(FunctionDeclSyntax.self),
|
|
62
|
+
let attribute = funcDecl.attributes.firstAttribute(named: "JS") {
|
|
63
|
+
entries.append(
|
|
64
|
+
buildClassFunctionEntry(funcDecl: funcDecl, attribute: attribute, typeName: typeName))
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if let varDecl = decl.as(VariableDeclSyntax.self),
|
|
69
|
+
let attribute = varDecl.attributes.firstAttribute(named: "JS") {
|
|
70
|
+
entries.append(
|
|
71
|
+
contentsOf: buildClassPropertyEntries(
|
|
72
|
+
varDecl: varDecl, attribute: attribute, typeName: typeName))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
let lines = entries.map { " \($0)" }.joined(separator: "\n")
|
|
77
|
+
let body = entries.isEmpty
|
|
78
|
+
? " return Class(\"\(jsName)\", \(typeName).self) {\n }"
|
|
79
|
+
: " return Class(\"\(jsName)\", \(typeName).self) {\n\(lines)\n }"
|
|
80
|
+
|
|
81
|
+
let method: DeclSyntax = """
|
|
82
|
+
public static func _synthesizedClassDefinition() -> ClassDefinition {
|
|
83
|
+
\(raw: body)
|
|
84
|
+
}
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
return [method]
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
extension SharedObjectMacro: MemberAttributeMacro {
|
|
92
|
+
public static func expansion(
|
|
93
|
+
of node: AttributeSyntax,
|
|
94
|
+
attachedTo declaration: some DeclGroupSyntax,
|
|
95
|
+
providingAttributesFor member: some DeclSyntaxProtocol,
|
|
96
|
+
in context: some MacroExpansionContext
|
|
97
|
+
) throws -> [AttributeSyntax] {
|
|
98
|
+
guard memberHasJSAttribute(member),
|
|
99
|
+
shouldStampJavaScriptActor(on: member, enclosedBy: declaration) else {
|
|
100
|
+
return []
|
|
101
|
+
}
|
|
102
|
+
return ["@JavaScriptActor"]
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// MARK: - Inheritance check
|
|
107
|
+
|
|
108
|
+
private func inheritsFromSharedObject(_ classDecl: ClassDeclSyntax) -> Bool {
|
|
109
|
+
return inheritsFromAny(classDecl, names: ["SharedObject"])
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// MARK: - Class-scope entry builders
|
|
113
|
+
|
|
114
|
+
private func buildClassFunctionEntry(
|
|
115
|
+
funcDecl: FunctionDeclSyntax,
|
|
116
|
+
attribute: AttributeSyntax,
|
|
117
|
+
typeName: String
|
|
118
|
+
) -> String {
|
|
119
|
+
let swiftName = funcDecl.name.text
|
|
120
|
+
let jsName = jsNameArgument(of: attribute) ?? swiftName
|
|
121
|
+
let effects = funcDecl.signature.effectSpecifiers
|
|
122
|
+
let isAsync = effects?.asyncSpecifier != nil
|
|
123
|
+
let isThrowing = effects?.throwsClause?.throwsSpecifier != nil
|
|
124
|
+
let dslEntry = isAsync ? "AsyncFunction" : "Function"
|
|
125
|
+
|
|
126
|
+
let params = funcDecl.signature.parameterClause.parameters
|
|
127
|
+
let closureParamList: String
|
|
128
|
+
let callArgList: String
|
|
129
|
+
if params.isEmpty {
|
|
130
|
+
closureParamList = "(this: \(typeName))"
|
|
131
|
+
callArgList = ""
|
|
132
|
+
} else {
|
|
133
|
+
let typedParams = params.enumerated().map { index, param in
|
|
134
|
+
"_ arg\(index): \(param.type.trimmedDescription)"
|
|
135
|
+
}.joined(separator: ", ")
|
|
136
|
+
closureParamList = "(this: \(typeName), \(typedParams))"
|
|
137
|
+
|
|
138
|
+
callArgList = params.enumerated().map { index, param in
|
|
139
|
+
let label = param.firstName.text
|
|
140
|
+
return label == "_" ? "arg\(index)" : "\(label): arg\(index)"
|
|
141
|
+
}.joined(separator: ", ")
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
let awaitKeyword = isAsync ? "await " : ""
|
|
145
|
+
let tryKeyword = (isAsync || isThrowing) ? "try " : ""
|
|
146
|
+
let callExpr = "\(tryKeyword)\(awaitKeyword)this.\(swiftName)(\(callArgList))"
|
|
147
|
+
|
|
148
|
+
return "\(dslEntry)(\"\(jsName)\") { \(closureParamList) in \(callExpr) }"
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private func buildClassPropertyEntries(
|
|
152
|
+
varDecl: VariableDeclSyntax,
|
|
153
|
+
attribute: AttributeSyntax,
|
|
154
|
+
typeName: String
|
|
155
|
+
) -> [String] {
|
|
156
|
+
let jsNameOverride = jsNameArgument(of: attribute)
|
|
157
|
+
|
|
158
|
+
return varDecl.bindings.compactMap { binding in
|
|
159
|
+
guard let ident = binding.pattern.as(IdentifierPatternSyntax.self) else {
|
|
160
|
+
return nil
|
|
161
|
+
}
|
|
162
|
+
let swiftName = ident.identifier.text
|
|
163
|
+
let jsName = jsNameOverride ?? swiftName
|
|
164
|
+
return "Property(\"\(jsName)\") { (this: \(typeName)) in this.\(swiftName) }"
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private func buildConstructorEntry(
|
|
169
|
+
initDecl: InitializerDeclSyntax,
|
|
170
|
+
typeName: String
|
|
171
|
+
) -> String {
|
|
172
|
+
let params = initDecl.signature.parameterClause.parameters
|
|
173
|
+
|
|
174
|
+
if params.isEmpty {
|
|
175
|
+
return "Constructor { \(typeName)() }"
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
let argList = params.enumerated().map { index, param in
|
|
179
|
+
"_ arg\(index): \(param.type.trimmedDescription)"
|
|
180
|
+
}.joined(separator: ", ")
|
|
181
|
+
|
|
182
|
+
let callArgs = params.enumerated().map { index, param in
|
|
183
|
+
let label = param.firstName.text
|
|
184
|
+
return label == "_" ? "arg\(index)" : "\(label): arg\(index)"
|
|
185
|
+
}.joined(separator: ", ")
|
|
186
|
+
|
|
187
|
+
return "Constructor { (\(argList)) in \(typeName)(\(callArgs)) }"
|
|
188
|
+
}
|
package/apple/build.js
CHANGED
|
@@ -1,21 +1,138 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
-
const
|
|
4
|
-
const fs = require('node:fs');
|
|
3
|
+
const childProcess = require('node:child_process');
|
|
4
|
+
const fs = require('node:fs/promises');
|
|
5
|
+
const os = require('node:os');
|
|
5
6
|
const path = require('node:path');
|
|
7
|
+
const { promisify } = require('node:util');
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
execSync('swift build -c release', { stdio: 'inherit', cwd: __dirname });
|
|
9
|
+
const execFile = promisify(childProcess.execFile);
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Runs a command and resolves when it exits successfully.
|
|
13
|
+
*/
|
|
14
|
+
function spawnAsync(command, args, options = {}) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const child = childProcess.spawn(command, args, { stdio: 'inherit', ...options });
|
|
17
|
+
child.once('error', reject);
|
|
18
|
+
child.once('close', (code, signal) => {
|
|
19
|
+
if (code === 0) {
|
|
20
|
+
resolve();
|
|
21
|
+
} else {
|
|
22
|
+
reject(new Error(`\`${command} ${args.join(' ')}\` exited with ${signal ?? `code ${code}`}`));
|
|
23
|
+
}
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Builds the macro plugin for the given architecture and returns the path to the built binary.
|
|
30
|
+
* SwiftPM always builds macro tools for the host architecture, so the x86_64 slice
|
|
31
|
+
* is built by running the whole toolchain under Rosetta with `arch -${arch}`.
|
|
32
|
+
*/
|
|
33
|
+
async function buildForArch(arch) {
|
|
34
|
+
const buildArgs = ['build', '-c', 'release'];
|
|
35
|
+
if (arch === os.machine()) {
|
|
36
|
+
await spawnAsync('swift', buildArgs, { cwd: __dirname });
|
|
37
|
+
} else {
|
|
38
|
+
await spawnAsync('arch', [`-${arch}`, 'swift', ...buildArgs], { cwd: __dirname });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const binPath = path.join(__dirname, `.build/${arch}-apple-macosx/release/ExpoModulesMacros-tool`);
|
|
42
|
+
try {
|
|
43
|
+
await fs.access(binPath);
|
|
44
|
+
} catch {
|
|
45
|
+
throw new Error(`Could not find the built ExpoModulesMacros-tool at ${binPath}`);
|
|
46
|
+
}
|
|
47
|
+
return binPath;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Checks whether the Swift toolchain actually runs for the given architecture by
|
|
52
|
+
* verifying the host target triple it reports, e.g. `Target: x86_64-apple-macosx26.0`
|
|
53
|
+
* under Rosetta. Exit status alone is not enough: if `arch` ever fell back to the
|
|
54
|
+
* native architecture, the probe would pass but the build would produce a wrong slice.
|
|
55
|
+
*/
|
|
56
|
+
async function canRunSwiftForArch(arch) {
|
|
57
|
+
try {
|
|
58
|
+
const { stdout } = await execFile('arch', [`-${arch}`, 'swift', '--version']);
|
|
59
|
+
return stdout.includes(`${arch}-apple`);
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Checks whether the toolchain can build for the given architecture.
|
|
67
|
+
* Building x86_64 on Apple Silicon requires Rosetta, so when it is missing,
|
|
68
|
+
* try to install it before giving up.
|
|
69
|
+
*/
|
|
70
|
+
async function canBuildForArch(arch) {
|
|
71
|
+
if (arch === os.machine()) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
if (await canRunSwiftForArch(arch)) {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
if (arch === 'x86_64' && os.machine() === 'arm64') {
|
|
78
|
+
try {
|
|
79
|
+
console.log('Installing Rosetta to build the x86_64 slice...');
|
|
80
|
+
await spawnAsync('sudo', ['softwareupdate', '--install-rosetta', '--agree-to-license']);
|
|
81
|
+
return await canRunSwiftForArch(arch);
|
|
82
|
+
} catch {
|
|
83
|
+
// No sudo access or the install failed - fall through to the unsupported arch warning.
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
11
88
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
89
|
+
/**
|
|
90
|
+
* Asserts that the built binary contains a slice for each of the given architectures.
|
|
91
|
+
*/
|
|
92
|
+
async function verifyArchs(binaryPath, archs) {
|
|
93
|
+
const { stdout } = await execFile('lipo', ['-archs', binaryPath]);
|
|
94
|
+
const builtArchs = stdout.trim().split(/\s+/);
|
|
95
|
+
for (const arch of archs) {
|
|
96
|
+
if (!builtArchs.includes(arch)) {
|
|
97
|
+
throw new Error(`The built binary at ${binaryPath} is missing the ${arch} slice`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
const outputPath = path.join(__dirname, 'ExpoModulesMacros-tool');
|
|
104
|
+
|
|
105
|
+
const archs = [];
|
|
106
|
+
for (const arch of ['arm64', 'x86_64']) {
|
|
107
|
+
if (await canBuildForArch(arch)) {
|
|
108
|
+
archs.push(arch);
|
|
109
|
+
} else {
|
|
110
|
+
console.warn(`The Swift toolchain cannot run for ${arch} - the built binary will not support ${arch} Macs.`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
if (archs.length === 0) {
|
|
114
|
+
throw new Error('The Swift toolchain is not available for any supported architecture');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Builds run sequentially as SwiftPM locks the shared .build directory.
|
|
118
|
+
const binPaths = [];
|
|
119
|
+
for (const arch of archs) {
|
|
120
|
+
binPaths.push(await buildForArch(arch));
|
|
121
|
+
}
|
|
16
122
|
|
|
17
|
-
|
|
18
|
-
|
|
123
|
+
await fs.rm(outputPath, { force: true });
|
|
124
|
+
if (binPaths.length > 1) {
|
|
125
|
+
await spawnAsync('lipo', ['-create', ...binPaths, '-output', outputPath]);
|
|
126
|
+
} else {
|
|
127
|
+
await fs.copyFile(binPaths[0], outputPath);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await spawnAsync('strip', [outputPath]);
|
|
131
|
+
await verifyArchs(outputPath, archs);
|
|
132
|
+
await spawnAsync('lipo', ['-info', outputPath]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
main().catch((error) => {
|
|
19
136
|
console.error('Build failed:', error.message);
|
|
20
137
|
process.exit(1);
|
|
21
|
-
}
|
|
138
|
+
});
|