expo-modules-macros 0.20.0 → 0.21.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/linux.yml +132 -0
- package/.github/workflows/publish.yml +160 -11
- package/.github/workflows/windows.yml +3 -28
- package/AGENTS.md +11 -6
- package/README.md +13 -7
- package/apple/ExpoModulesMacros +0 -0
- package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +5 -0
- package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +2 -0
- package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +64 -0
- package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +2 -1
- package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +150 -90
- package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +3 -0
- package/build/index.d.ts +4 -3
- package/build/index.js +33 -18
- package/package.json +5 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
name: Linux
|
|
2
|
+
|
|
3
|
+
# Builds and tests the package on Linux, and checks the release tool and its package there.
|
|
4
|
+
|
|
5
|
+
on:
|
|
6
|
+
push:
|
|
7
|
+
branches: [main]
|
|
8
|
+
pull_request:
|
|
9
|
+
branches: [main]
|
|
10
|
+
workflow_dispatch: {}
|
|
11
|
+
|
|
12
|
+
concurrency:
|
|
13
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}
|
|
14
|
+
cancel-in-progress: true
|
|
15
|
+
|
|
16
|
+
defaults:
|
|
17
|
+
run:
|
|
18
|
+
working-directory: apple
|
|
19
|
+
shell: bash
|
|
20
|
+
|
|
21
|
+
jobs:
|
|
22
|
+
linux:
|
|
23
|
+
name: Build & test (Linux ${{ matrix.arch }})
|
|
24
|
+
runs-on: ${{ matrix.runner }}
|
|
25
|
+
# The release tool needs the glibc version that it's built against or a newer one. Red Hat UBI 9
|
|
26
|
+
# has glibc 2.34, the oldest version that the package supports. The Publish workflow builds the
|
|
27
|
+
# tool in the same image.
|
|
28
|
+
container: swift:6.4.0-rhel-ubi9@sha256:802a003f1eacbca9c20664a8ba79621d13f4538c3d329e745b7f8d53c70604a3
|
|
29
|
+
# The limit stops a hang, for example in the plugin server, from holding a runner for the default
|
|
30
|
+
# 6 hours.
|
|
31
|
+
timeout-minutes: 30
|
|
32
|
+
env:
|
|
33
|
+
# The newest glibc symbol version that the release tool may need. The READMEs of the Linux
|
|
34
|
+
# packages name it.
|
|
35
|
+
MAX_GLIBC_VERSION: '2.34'
|
|
36
|
+
strategy:
|
|
37
|
+
fail-fast: false
|
|
38
|
+
matrix:
|
|
39
|
+
include:
|
|
40
|
+
- arch: x64
|
|
41
|
+
runner: ubuntu-24.04
|
|
42
|
+
- arch: arm64
|
|
43
|
+
runner: ubuntu-24.04-arm
|
|
44
|
+
steps:
|
|
45
|
+
- name: Checkout
|
|
46
|
+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
47
|
+
|
|
48
|
+
- name: Setup Node
|
|
49
|
+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
50
|
+
with:
|
|
51
|
+
node-version: 24
|
|
52
|
+
|
|
53
|
+
- name: Cache SwiftPM build
|
|
54
|
+
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
|
|
55
|
+
with:
|
|
56
|
+
path: apple/.build
|
|
57
|
+
key: spm-${{ github.repository }}-${{ runner.os }}-${{ matrix.arch }}-${{ hashFiles('apple/Package.resolved') }}
|
|
58
|
+
restore-keys: |
|
|
59
|
+
spm-${{ github.repository }}-${{ runner.os }}-${{ matrix.arch }}-
|
|
60
|
+
|
|
61
|
+
- name: Build
|
|
62
|
+
run: swift build
|
|
63
|
+
|
|
64
|
+
- name: Test
|
|
65
|
+
run: swift test
|
|
66
|
+
|
|
67
|
+
- name: Build release tool
|
|
68
|
+
# Runs `npm run build` (scripts/build.js): builds the release plugin binary for this
|
|
69
|
+
# architecture as platforms/linux-<arch>/ExpoModulesMacros.
|
|
70
|
+
working-directory: ${{ github.workspace }}
|
|
71
|
+
run: npm run build
|
|
72
|
+
|
|
73
|
+
- name: Verify the release tool
|
|
74
|
+
# With no arguments it is the plugin server, which exits when stdin closes; with arguments it
|
|
75
|
+
# is the scanner CLI.
|
|
76
|
+
run: |
|
|
77
|
+
tool="../platforms/linux-${{ matrix.arch }}/ExpoModulesMacros"
|
|
78
|
+
# The Swift runtime must be linked in statically, so no Swift library may be a dependency.
|
|
79
|
+
needed="$(readelf --dynamic "$tool" | sed -n 's/.*(NEEDED).*\[\(.*\)\]$/\1/p')"
|
|
80
|
+
echo "Needed libraries:" $needed
|
|
81
|
+
if echo "$needed" | grep -E 'swift|Foundation|dispatch|BlocksRuntime'; then
|
|
82
|
+
echo "The release tool links a Swift library dynamically"
|
|
83
|
+
exit 1
|
|
84
|
+
fi
|
|
85
|
+
glibc="$(objdump -T "$tool" | grep -o 'GLIBC_[0-9][0-9.]*' | sed 's/^GLIBC_//' | sort -uV | tail -n 1)"
|
|
86
|
+
echo "Newest glibc symbol version: $glibc"
|
|
87
|
+
if [ "$(printf '%s\n' "$glibc" "$MAX_GLIBC_VERSION" | sort -V | tail -n 1)" != "$MAX_GLIBC_VERSION" ]; then
|
|
88
|
+
echo "The release tool needs glibc $glibc, newer than $MAX_GLIBC_VERSION"
|
|
89
|
+
exit 1
|
|
90
|
+
fi
|
|
91
|
+
"$tool" < /dev/null
|
|
92
|
+
"$tool" --help > /dev/null
|
|
93
|
+
|
|
94
|
+
- name: Pack the platform package
|
|
95
|
+
# Checks what `npm pack` would publish from `platforms/linux-<arch>`, where the release build
|
|
96
|
+
# put the tool: the executable with its executable bit, the README, the license and the
|
|
97
|
+
# manifest, with the `os`, `cpu` and `libc` fields for this machine.
|
|
98
|
+
working-directory: ${{ github.workspace }}
|
|
99
|
+
run: |
|
|
100
|
+
# The `./` matters: npm reads `platforms/linux-<arch>` as a GitHub repository.
|
|
101
|
+
npm pack "./platforms/linux-${{ matrix.arch }}" --pack-destination "$RUNNER_TEMP" --json > "$RUNNER_TEMP/pack.json"
|
|
102
|
+
node - "$RUNNER_TEMP/pack.json" "${{ matrix.arch }}" <<'EOF'
|
|
103
|
+
const fs = require('node:fs');
|
|
104
|
+
const [packPath, arch] = process.argv.slice(2);
|
|
105
|
+
const [pack] = JSON.parse(fs.readFileSync(packPath, 'utf8'));
|
|
106
|
+
const files = pack.files.map((file) => file.path).sort().join(',');
|
|
107
|
+
console.log(`${pack.filename}: ${files} (${pack.size} bytes packed)`);
|
|
108
|
+
if (files !== 'ExpoModulesMacros,LICENSE,README.md,package.json') {
|
|
109
|
+
throw new Error(`Unexpected files: ${files}`);
|
|
110
|
+
}
|
|
111
|
+
const tool = pack.files.find((file) => file.path === 'ExpoModulesMacros');
|
|
112
|
+
if ((tool.mode & 0o111) === 0) {
|
|
113
|
+
throw new Error(`ExpoModulesMacros is packed without the executable bit (mode ${tool.mode.toString(8)})`);
|
|
114
|
+
}
|
|
115
|
+
const manifest = JSON.parse(fs.readFileSync(`platforms/linux-${arch}/package.json`, 'utf8'));
|
|
116
|
+
const fields = [manifest.os, manifest.cpu, manifest.libc].join(' / ');
|
|
117
|
+
if (fields !== `linux / ${arch} / glibc`) {
|
|
118
|
+
throw new Error(`Unexpected os / cpu / libc: ${fields}`);
|
|
119
|
+
}
|
|
120
|
+
fs.appendFileSync(process.env.GITHUB_ENV, `PLATFORM_PACKAGE_TARBALL=${process.env.RUNNER_TEMP}/${pack.filename}\n`);
|
|
121
|
+
EOF
|
|
122
|
+
|
|
123
|
+
- name: Scan through the TypeScript wrapper
|
|
124
|
+
# Installs the packed platform package. The wrapper must find the executable in
|
|
125
|
+
# `@expo/modules-macros-linux-<arch>`, as it would in a project, not the local build.
|
|
126
|
+
working-directory: ${{ github.workspace }}
|
|
127
|
+
run: |
|
|
128
|
+
npm install
|
|
129
|
+
npm install --no-save "$PLATFORM_PACKAGE_TARBALL"
|
|
130
|
+
npm run build:ts
|
|
131
|
+
node scripts/check-scan.js .
|
|
132
|
+
|
|
@@ -70,11 +70,147 @@ jobs:
|
|
|
70
70
|
path: platforms/win32-${{ matrix.arch }}/ExpoModulesMacros.exe
|
|
71
71
|
if-no-files-found: error
|
|
72
72
|
|
|
73
|
+
build-linux:
|
|
74
|
+
name: Build Linux tool (${{ matrix.arch }})
|
|
75
|
+
# SwiftPM builds macro tools only for the host, so each architecture builds on a runner of its own.
|
|
76
|
+
runs-on: ${{ matrix.runner }}
|
|
77
|
+
# The tool needs the glibc version that it's built against or a newer one. Red Hat UBI 9 has
|
|
78
|
+
# glibc 2.34, the oldest version that the package supports. The Linux workflow uses the same image.
|
|
79
|
+
container: swift:6.4.0-rhel-ubi9@sha256:802a003f1eacbca9c20664a8ba79621d13f4538c3d329e745b7f8d53c70604a3
|
|
80
|
+
timeout-minutes: 30
|
|
81
|
+
strategy:
|
|
82
|
+
matrix:
|
|
83
|
+
include:
|
|
84
|
+
- arch: x64
|
|
85
|
+
runner: ubuntu-24.04
|
|
86
|
+
- arch: arm64
|
|
87
|
+
runner: ubuntu-24.04-arm
|
|
88
|
+
steps:
|
|
89
|
+
- name: Checkout
|
|
90
|
+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
91
|
+
|
|
92
|
+
- name: Setup Node
|
|
93
|
+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
94
|
+
with:
|
|
95
|
+
node-version: 24
|
|
96
|
+
|
|
97
|
+
- name: Build release tool
|
|
98
|
+
# Runs scripts/build.js: builds the release plugin binary for this architecture as
|
|
99
|
+
# platforms/linux-<arch>/ExpoModulesMacros.
|
|
100
|
+
run: npm run build
|
|
101
|
+
|
|
102
|
+
- name: Upload tool
|
|
103
|
+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
|
104
|
+
with:
|
|
105
|
+
# Named after the package directory, like the Windows artifacts.
|
|
106
|
+
name: linux-${{ matrix.arch }}
|
|
107
|
+
path: platforms/linux-${{ matrix.arch }}/ExpoModulesMacros
|
|
108
|
+
if-no-files-found: error
|
|
109
|
+
|
|
110
|
+
verify-windows:
|
|
111
|
+
name: Verify Windows tool (${{ matrix.arch }})
|
|
112
|
+
needs: build-windows
|
|
113
|
+
# Installs the release tool from its packed package and scans through the TypeScript wrapper,
|
|
114
|
+
# before anything is published. The runner has no Swift toolchain.
|
|
115
|
+
runs-on: ${{ matrix.runner }}
|
|
116
|
+
timeout-minutes: 15
|
|
117
|
+
strategy:
|
|
118
|
+
matrix:
|
|
119
|
+
include:
|
|
120
|
+
- arch: x64
|
|
121
|
+
runner: windows-latest
|
|
122
|
+
- arch: arm64
|
|
123
|
+
runner: windows-11-arm
|
|
124
|
+
steps:
|
|
125
|
+
- name: Checkout
|
|
126
|
+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
127
|
+
|
|
128
|
+
- name: Setup Node
|
|
129
|
+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
130
|
+
with:
|
|
131
|
+
node-version: 24
|
|
132
|
+
|
|
133
|
+
- name: Download tool
|
|
134
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
135
|
+
with:
|
|
136
|
+
name: win32-${{ matrix.arch }}
|
|
137
|
+
path: platforms/win32-${{ matrix.arch }}
|
|
138
|
+
|
|
139
|
+
- name: Install the packed tool and scan
|
|
140
|
+
shell: pwsh
|
|
141
|
+
run: |
|
|
142
|
+
# The `./` matters: npm reads `platforms/win32-<arch>` as a GitHub repository.
|
|
143
|
+
$pack = npm pack "./platforms/win32-${{ matrix.arch }}" --pack-destination $env:RUNNER_TEMP --json | ConvertFrom-Json
|
|
144
|
+
if ($LASTEXITCODE -ne 0) { throw "npm pack failed" }
|
|
145
|
+
npm install
|
|
146
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
147
|
+
npm install --no-save (Join-Path $env:RUNNER_TEMP $pack[0].filename)
|
|
148
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
149
|
+
npm run build:ts
|
|
150
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
151
|
+
node scripts/check-scan.js .
|
|
152
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
153
|
+
|
|
154
|
+
verify-linux:
|
|
155
|
+
name: Verify Linux tool (${{ matrix.arch }})
|
|
156
|
+
needs: build-linux
|
|
157
|
+
# Installs the release tool from its packed package, then scans through the TypeScript wrapper in
|
|
158
|
+
# images of the oldest distribution releases that the package supports, before anything is
|
|
159
|
+
# published. The images have no Swift toolchain, and the runner's Node runs in them.
|
|
160
|
+
runs-on: ${{ matrix.runner }}
|
|
161
|
+
timeout-minutes: 15
|
|
162
|
+
defaults:
|
|
163
|
+
run:
|
|
164
|
+
shell: bash
|
|
165
|
+
strategy:
|
|
166
|
+
matrix:
|
|
167
|
+
include:
|
|
168
|
+
- arch: x64
|
|
169
|
+
runner: ubuntu-24.04
|
|
170
|
+
- arch: arm64
|
|
171
|
+
runner: ubuntu-24.04-arm
|
|
172
|
+
steps:
|
|
173
|
+
- name: Checkout
|
|
174
|
+
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
|
|
175
|
+
|
|
176
|
+
- name: Setup Node
|
|
177
|
+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
|
|
178
|
+
with:
|
|
179
|
+
node-version: 24
|
|
180
|
+
|
|
181
|
+
- name: Download tool
|
|
182
|
+
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
183
|
+
with:
|
|
184
|
+
name: linux-${{ matrix.arch }}
|
|
185
|
+
path: platforms/linux-${{ matrix.arch }}
|
|
186
|
+
|
|
187
|
+
- name: Install the packed tool
|
|
188
|
+
run: |
|
|
189
|
+
# Artifacts don't keep file modes, so the executable gets its executable bit back first.
|
|
190
|
+
chmod +x platforms/linux-${{ matrix.arch }}/ExpoModulesMacros
|
|
191
|
+
# The `./` matters: npm reads `platforms/linux-<arch>` as a GitHub repository.
|
|
192
|
+
npm pack "./platforms/linux-${{ matrix.arch }}" --pack-destination "$RUNNER_TEMP" --json > "$RUNNER_TEMP/pack.json"
|
|
193
|
+
tarball="$(node -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1], "utf8"))[0].filename' "$RUNNER_TEMP/pack.json")"
|
|
194
|
+
npm install
|
|
195
|
+
npm install --no-save "$RUNNER_TEMP/$tarball"
|
|
196
|
+
npm run build:ts
|
|
197
|
+
|
|
198
|
+
- name: Scan on each distribution
|
|
199
|
+
run: |
|
|
200
|
+
node_dir="$(dirname "$(dirname "$(command -v node)")")"
|
|
201
|
+
for image in ubuntu:22.04 debian:12 amazonlinux:2023; do
|
|
202
|
+
echo "::group::$image"
|
|
203
|
+
run=(docker run --rm -v "$PWD:/work:ro" -v "$node_dir:/node:ro" -w /work "$image")
|
|
204
|
+
"${run[@]}" getconf GNU_LIBC_VERSION
|
|
205
|
+
"${run[@]}" /node/bin/node scripts/check-scan.js .
|
|
206
|
+
echo "::endgroup::"
|
|
207
|
+
done
|
|
208
|
+
|
|
73
209
|
publish:
|
|
74
210
|
name: Build and publish to npm
|
|
75
|
-
needs: build-windows
|
|
76
|
-
# Builds the native macOS tool binary, so it must run on macOS. The Windows binaries come
|
|
77
|
-
# build-windows jobs.
|
|
211
|
+
needs: [build-windows, build-linux, verify-windows, verify-linux]
|
|
212
|
+
# Builds the native macOS tool binary, so it must run on macOS. The Windows and Linux binaries come
|
|
213
|
+
# from the build-windows and build-linux jobs.
|
|
78
214
|
# The package declares swift-tools-version 6.2, which requires Xcode 26+.
|
|
79
215
|
runs-on: macos-26
|
|
80
216
|
permissions:
|
|
@@ -151,25 +287,38 @@ jobs:
|
|
|
151
287
|
# copies it to apple/ExpoModulesMacros, and strips it.
|
|
152
288
|
run: npm run build
|
|
153
289
|
|
|
154
|
-
- name:
|
|
155
|
-
#
|
|
290
|
+
- name: Verify the macOS tool
|
|
291
|
+
# Scans through the TypeScript wrapper, which must find the binary in `apple/`, before
|
|
292
|
+
# anything is published. The verify-windows and verify-linux jobs check the other binaries.
|
|
293
|
+
run: |
|
|
294
|
+
npm run build:ts
|
|
295
|
+
node scripts/check-scan.js .
|
|
296
|
+
|
|
297
|
+
- name: Download Windows and Linux tools
|
|
298
|
+
# Each artifact goes into the directory of its name: `platforms/<platform>-<arch>`.
|
|
156
299
|
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
|
|
157
300
|
with:
|
|
158
|
-
pattern: win32-*
|
|
301
|
+
pattern: '{win32,linux}-*'
|
|
159
302
|
path: platforms
|
|
160
303
|
|
|
161
|
-
- name: Verify Windows tools
|
|
162
|
-
|
|
304
|
+
- name: Verify Windows and Linux tools
|
|
305
|
+
# Artifacts don't keep file modes, so the Linux executables get their executable bit back
|
|
306
|
+
# here. `npm publish` keeps it in the package.
|
|
307
|
+
run: |
|
|
308
|
+
chmod +x platforms/linux-x64/ExpoModulesMacros platforms/linux-arm64/ExpoModulesMacros
|
|
309
|
+
ls -l \
|
|
310
|
+
platforms/win32-x64/ExpoModulesMacros.exe platforms/win32-arm64/ExpoModulesMacros.exe \
|
|
311
|
+
platforms/linux-x64/ExpoModulesMacros platforms/linux-arm64/ExpoModulesMacros
|
|
163
312
|
|
|
164
|
-
- name: Publish the Windows packages
|
|
313
|
+
- name: Publish the Windows and Linux packages
|
|
165
314
|
# Published before `expo-modules-macros`, whose optional dependencies point at them, so that
|
|
166
315
|
# package is never on npm without them. Each package needs trusted publishing (OIDC)
|
|
167
316
|
# configured on npmjs.com for this workflow.
|
|
168
317
|
env:
|
|
169
318
|
DRY_RUN: ${{ inputs.dry_run && '--dry-run' || '' }}
|
|
170
319
|
run: |
|
|
171
|
-
for
|
|
172
|
-
npm publish --access public --tag "${{ steps.bump.outputs.tag }}" $DRY_RUN "./platforms
|
|
320
|
+
for package in win32-x64 win32-arm64 linux-x64 linux-arm64; do
|
|
321
|
+
npm publish --access public --tag "${{ steps.bump.outputs.tag }}" $DRY_RUN "./platforms/$package"
|
|
173
322
|
done
|
|
174
323
|
|
|
175
324
|
- name: Publish
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
name: Windows
|
|
2
2
|
|
|
3
|
-
# Builds and tests the package on Windows, and checks the release tool
|
|
4
|
-
# aren't published yet, so this runs apart from the macOS workflow.
|
|
3
|
+
# Builds and tests the package on Windows, and checks the release tool and its package there.
|
|
5
4
|
|
|
6
5
|
on:
|
|
7
6
|
push:
|
|
@@ -116,29 +115,5 @@ jobs:
|
|
|
116
115
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
117
116
|
npm run build:ts
|
|
118
117
|
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
Set-Content (Join-Path $fixture "Module.swift") "@ExpoModule`npublic final class WindowsModule {}"
|
|
122
|
-
@'
|
|
123
|
-
const path = require('node:path');
|
|
124
|
-
const { getScannerBinaryPath, scanModules } = require(path.resolve('build'));
|
|
125
|
-
const binaryPath = getScannerBinaryPath();
|
|
126
|
-
const expected = path.join('node_modules', '@expo', `modules-macros-win32-${process.arch}`, 'ExpoModulesMacros.exe');
|
|
127
|
-
if (!binaryPath.endsWith(expected)) {
|
|
128
|
-
console.error(`getScannerBinaryPath() returned ${binaryPath}, not a path ending in ${expected}`);
|
|
129
|
-
process.exit(1);
|
|
130
|
-
}
|
|
131
|
-
console.log(`getScannerBinaryPath() returned ${binaryPath}`);
|
|
132
|
-
scanModules([process.argv[2]]).then((result) => {
|
|
133
|
-
const module = result.modules.find((candidate) => candidate.name === 'WindowsModule');
|
|
134
|
-
if (!module) {
|
|
135
|
-
console.error(JSON.stringify(result, null, 2));
|
|
136
|
-
process.exit(1);
|
|
137
|
-
}
|
|
138
|
-
console.log(`scanModules reported ${module.name} in ${module.file}`);
|
|
139
|
-
}, (error) => {
|
|
140
|
-
console.error(error);
|
|
141
|
-
process.exit(1);
|
|
142
|
-
});
|
|
143
|
-
'@ | Set-Content (Join-Path $env:RUNNER_TEMP "scan-check.js")
|
|
144
|
-
node (Join-Path $env:RUNNER_TEMP "scan-check.js") $fixture
|
|
118
|
+
node scripts/check-scan.js .
|
|
119
|
+
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
|
package/AGENTS.md
CHANGED
|
@@ -9,10 +9,10 @@
|
|
|
9
9
|
- `apple/Sources/ExpoModulesScanner`: the scanner library. `Modules/` implements `scan-modules` (autolinking), `Exports/` implements `scan-exports` (type generation), `Core/` holds the shared parsing and `#if` evaluation, and `CLI.swift` is the command-line front end.
|
|
10
10
|
- `apple/Sources/ExpoModulesOptimized`: declarations for the `@OptimizedFunction` macro.
|
|
11
11
|
- `apple/Tests`: `ExpoModulesMacrosTests` (expansion tests) and `ExpoModulesScannerTests`.
|
|
12
|
-
- `platforms/`: the Windows packages `@expo/modules-macros-win32-x64` and `-arm64` (a `package.json` with `os` and `cpu`, a README and a copy of `LICENSE`). The Publish workflow runs `scripts/set-platform-versions.js` to give them the version of this package. Their `ExpoModulesMacros.exe` is written there by `npm run build` on
|
|
12
|
+
- `platforms/`: the Windows packages `@expo/modules-macros-win32-x64` and `-arm64`, and the Linux packages `@expo/modules-macros-linux-x64` and `-arm64` (a `package.json` with `os` and `cpu`, plus `libc` on Linux, a README and a copy of `LICENSE`). The Publish workflow runs `scripts/set-platform-versions.js` to give them the version of this package. Their `ExpoModulesMacros.exe` (Windows) or `ExpoModulesMacros` (Linux) is written there by `npm run build` on that platform and isn't committed.
|
|
13
13
|
- `apple/PodTests`: a stub test so that `expo/expo` native tests can install this package as a pod. Do not add real tests here.
|
|
14
14
|
- `src/`: the TypeScript wrapper around the scanner CLI. `types.ts` mirrors the Swift `Codable` output types by hand.
|
|
15
|
-
- `scripts/build.js` (`npm run build`): builds the release binaries: the universal macOS `apple/ExpoModulesMacros` (committed and published), and on Windows `platforms/win32-<arch>/ExpoModulesMacros.exe` for the host architecture (not committed; published in that package).
|
|
15
|
+
- `scripts/build.js` (`npm run build`): builds the release binaries: the universal macOS `apple/ExpoModulesMacros` (committed and published), and on Windows and Linux `platforms/win32-<arch>/ExpoModulesMacros.exe` or `platforms/linux-<arch>/ExpoModulesMacros` for the host architecture (not committed; published in that package).
|
|
16
16
|
|
|
17
17
|
## Commands
|
|
18
18
|
|
|
@@ -25,13 +25,18 @@ npm run typecheck # TypeScript wrapper
|
|
|
25
25
|
npm run build # release binary (slow; on macOS uses Rosetta for x86_64)
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
CI runs on macOS (`.github/workflows/swift.yml`)
|
|
28
|
+
CI runs on macOS (`.github/workflows/swift.yml`), on Windows x64 and arm64 (`.github/workflows/windows.yml`), and on Linux x64 and arm64 (`.github/workflows/linux.yml`). On macOS it runs the release build, checks both binary slices, then runs `swift test` and `npm run typecheck`. On Windows it builds and tests the package, builds the release binary, checks that it starts without the Swift runtime, packs it into its `platforms/` package (checking the files and the `os`/`cpu` fields), installs that package, and scans through the TypeScript wrapper with `scripts/check-scan.js`, which must find the executable there. On Linux it does the same in the `swift:6.4.0-rhel-ubi9` image, and also checks that no Swift library is linked dynamically, the newest glibc symbol version that the binary needs, and that the package keeps the executable bit. Code in `Sources` and `Tests` must not assume Apple platforms or POSIX paths.
|
|
29
29
|
|
|
30
30
|
## Windows
|
|
31
31
|
|
|
32
32
|
- Outside Apple platforms, `Sources` import `FoundationEssentials` instead of `Foundation` (`#if canImport(FoundationEssentials)`). Full Foundation brings in ICU, which would add tens of megabytes to the statically linked Windows binary. Don't use APIs that only `Foundation` has (`FileHandle`, `NSRegularExpression`, `CharacterSet`, `FileManager.enumerator`, `NSString` path methods); `Core/StandardStreams.swift` writes to stdout and stderr.
|
|
33
33
|
- The Windows binaries link the Swift runtime statically (`-static-stdlib`), because Swift has no stable ABI on Windows and the scanner must also run without a Swift toolchain. SwiftPM's `--static-swift-stdlib` has no effect there.
|
|
34
34
|
|
|
35
|
+
## Linux
|
|
36
|
+
|
|
37
|
+
- The `FoundationEssentials` rule above applies on Linux too.
|
|
38
|
+
- The Linux binaries link the Swift runtime statically (`-static-stdlib`; SwiftPM's `--static-swift-stdlib` doesn't apply to the macro tool here either) but link glibc and the GNU C++ runtime dynamically, so they need the glibc version they were built against or a newer one. CI and the Publish workflow build them in `swift:6.4.0-rhel-ubi9` (glibc 2.34), and the Linux workflow fails when the binary needs a newer glibc symbol than `MAX_GLIBC_VERSION`. A change to the build image must keep these and the glibc version in the READMEs in sync.
|
|
39
|
+
|
|
35
40
|
## Conventions
|
|
36
41
|
|
|
37
42
|
- Swift uses 2-space indentation. Write new doc comments as `///` line comments, not `/** */` blocks.
|
|
@@ -52,14 +57,14 @@ CI runs on macOS (`.github/workflows/swift.yml`) and on Windows x64 and arm64 (`
|
|
|
52
57
|
- **Macro declarations live in `expo-modules-core`** (`ios/Core/ExpoModulesMacros.swift` in `expo/expo`), as `#externalMacro(module: "ExpoModulesMacros", type: ...)`. Adding a macro, renaming a macro type or changing a macro's signature needs a matching edit there, and the type must be listed in `providingMacros` in `Plugin.swift`.
|
|
53
58
|
- **Generated code calls `expo-modules-core` API.** An expansion that uses a new core symbol only works with a core version that has it.
|
|
54
59
|
- **Scanner output is versioned.** `scanModulesSchemaVersion` (`Modules/ScanModules.swift`) and `scanExportsSchemaVersion` (`Exports/ExportedSurface.swift`) change independently. When the output shape of a command changes, bump its version and update the matching `SUPPORTED_SCAN_*_SCHEMA_VERSION` constant and mirror types in `src/types.ts`. `expo-modules-autolinking` also checks the `scan-modules` version.
|
|
55
|
-
- **The binary path is part of the contract.** `expo-modules-autolinking` resolves this package from `expo-modules-core` and passes `-load-plugin-executable <package>/apple/<binary>#ExpoModulesMacros` to the compiler. The scanner wrapper in autolinking uses the same path, and `expo-modules-cli` uses `getScannerBinaryPath()`. On Windows the binary is `ExpoModulesMacros.exe` in `@expo/modules-macros-win32-<arch>`. Renaming or moving a binary needs a matching change in `expo/expo`.
|
|
60
|
+
- **The binary path is part of the contract.** `expo-modules-autolinking` resolves this package from `expo-modules-core` and passes `-load-plugin-executable <package>/apple/<binary>#ExpoModulesMacros` to the compiler. The scanner wrapper in autolinking uses the same path, and `expo-modules-cli` uses `getScannerBinaryPath()`. On Windows the binary is `ExpoModulesMacros.exe` in `@expo/modules-macros-win32-<arch>`, and on Linux `ExpoModulesMacros` in `@expo/modules-macros-linux-<arch>`. Renaming or moving a binary needs a matching change in `expo/expo`.
|
|
56
61
|
|
|
57
62
|
## Releases
|
|
58
63
|
|
|
59
|
-
- Do not rebuild or commit the binary under `apple/` in a feature change. Only the **Publish** workflow (`.github/workflows/publish.yml`, manual) rebuilds it, and it commits the result as `Release vX.Y.Z` after the npm publish succeeds. It also builds the Windows binaries and publishes them in the `platforms/` packages, with the same version.
|
|
64
|
+
- Do not rebuild or commit the binary under `apple/` in a feature change. Only the **Publish** workflow (`.github/workflows/publish.yml`, manual) rebuilds it, and it commits the result as `Release vX.Y.Z` after the npm publish succeeds. It also builds the Windows and Linux binaries and publishes them in the `platforms/` packages, with the same version. Before it publishes anything, it installs each binary from its packed package and scans through the TypeScript wrapper with `scripts/check-scan.js`.
|
|
60
65
|
- Do not edit the `version` in `package.json` by hand. The Publish workflow bumps it.
|
|
61
66
|
- Commit and PR titles are short and imperative, with code identifiers in backticks, for example ``Report `@Union` types in `scan-exports` ``.
|
|
62
67
|
|
|
63
68
|
## Installed copies
|
|
64
69
|
|
|
65
|
-
In `node_modules`, the compiler runs the prebuilt binary `apple/ExpoModulesMacros` (on Windows,
|
|
70
|
+
In `node_modules`, the compiler runs the prebuilt binary `apple/ExpoModulesMacros` (on Windows and Linux, the binary from `@expo/modules-macros-<platform>-<arch>`); editing the Swift sources there has no effect. Macro and scanner fixes belong in this repository and ship in a new release. The TypeScript wrapper ships compiled in `build/`, since `src/` is not published.
|
package/README.md
CHANGED
|
@@ -15,12 +15,17 @@ The same executable doubles as a source scanner CLI.
|
|
|
15
15
|
|
|
16
16
|
This package is not meant to be installed directly. It is a dependency of `expo-modules-core`, so every Expo project already has it. The published package contains a prebuilt universal (arm64 + x86_64) macOS binary at `apple/ExpoModulesMacros`, so consumers never build the plugin themselves.
|
|
17
17
|
|
|
18
|
-
The Windows binaries ship in their own packages,
|
|
18
|
+
The Windows and Linux binaries ship in their own packages, one per platform and architecture:
|
|
19
|
+
|
|
20
|
+
- `@expo/modules-macros-win32-x64` and `@expo/modules-macros-win32-arm64`, each with an `ExpoModulesMacros.exe`. They need only Windows system libraries and the Microsoft Visual C++ runtime.
|
|
21
|
+
- `@expo/modules-macros-linux-x64` and `@expo/modules-macros-linux-arm64`, each with an `ExpoModulesMacros`. They need glibc 2.34 or newer and the GNU C++ runtime (`libstdc++` and `libgcc_s`), for example on Ubuntu 22.04, Debian 12, Red Hat Enterprise Linux 9 or Amazon Linux 2023. They don't run on musl distributions such as Alpine Linux.
|
|
22
|
+
|
|
23
|
+
This package lists them as optional dependencies, and their `os`, `cpu` and (on Linux) `libc` fields make npm, pnpm and Yarn install only the one that matches the machine, so other machines don't download them. The Swift runtime is linked in statically, so the executables run without a Swift toolchain.
|
|
19
24
|
|
|
20
25
|
# Macros
|
|
21
26
|
|
|
22
27
|
- **`@ExpoModule(_ name: String? = nil, classes: [Any.Type] = [])`** on a class. Turns the class into a module: binds its `@JS` members into the module's JavaScript object and resolves the module name from the argument, falling back to the class name. It also synthesizes everything inheriting from `Module` used to provide, so a module class can carry any superclass, or none.
|
|
23
|
-
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. The macro also checks that every type crossing the boundary is convertible in the direction it travels, so a bad type is reported on the author's own declaration rather than on the enclosing type. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. A function or initializer can take a closure argument, such as `(Point) throws -> Size` or `(String) async throws -> Data`. JavaScript passes a function for it, and native code can store the closure and call it from any thread. A closure that throws or returns a value blocks the caller until JavaScript returns, and an `async` closure suspends instead and awaits a promise that the function returns. A closure that returns a value must be `throws`, so that an error from JavaScript can reach the caller. A non-throwing `Void` closure returns before JavaScript reads its arguments, so don't change a class instance after passing it to one.
|
|
28
|
+
- **`@JS(_ jsName: String? = nil, _ options: JSOptions...)`** on a member of a module or shared object. Marks a function, property or initializer for export. `@ExpoModule` and `@SharedObject` bind each marked member straight into the JavaScript object, with the argument decoding, the call and the result encoding inlined, so there is no dynamic per-call path. The JS name defaults to the Swift name; pass a string to override it. Two `@JS` members with the same JS name on one JavaScript object are a compile-time error, since JavaScript could reach only one of them. A shared object's static and instance members bind onto different objects, so they can share a name. The macro also checks that every type crossing the boundary is convertible in the direction it travels, so a bad type is reported on the author's own declaration rather than on the enclosing type. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. A function or initializer can take a closure argument, such as `(Point) throws -> Size` or `(String) async throws -> Data`. JavaScript passes a function for it, and native code can store the closure and call it from any thread. A closure that throws or returns a value blocks the caller until JavaScript returns, and an `async` closure suspends instead and awaits a promise that the function returns. A closure that returns a value must be `throws`, so that an error from JavaScript can reach the caller. A non-throwing `Void` closure returns before JavaScript reads its arguments, so don't change a class instance after passing it to one.
|
|
24
29
|
- **`@Event(_ name: String? = nil, sync: Bool = false)`** on a function-typed `var`. Expands the property into a closure that emits the event, so calling the property sends it to JavaScript with the closure's parameter as the payload. The JS name defaults to the property name with a leading `on` stripped. `sync: true` dispatches inline instead of asynchronously.
|
|
25
30
|
- **`@SharedObject(_ name: String? = nil)`** on a `SharedObject` subclass. Collects the class's `@JS` members into a class definition that a module exposes through `@ExpoModule(classes:)`.
|
|
26
31
|
- **`@Record()`** on a record type. Treats every non-static, non-private, non-computed stored property as a field, with no per-field wrapper, and synthesizes the memberwise initializer, the conversions in both directions, and the `Record` conformance. Requiredness is inferred: a default value makes a field optional, an optional type makes it nullable and optional.
|
|
@@ -97,13 +102,13 @@ The binary is a compiled executable, so each call still spawns a process. What t
|
|
|
97
102
|
|
|
98
103
|
to `OTHER_SWIFT_FLAGS` for `ExpoModulesCore`, every pod that depends on it, and their test specs. Expo's SPM prebuilds pass the same flag when they generate `Package.swift`, so both build systems load the same binary.
|
|
99
104
|
|
|
100
|
-
On Windows, the compiler loads `ExpoModulesMacros.exe` from `@expo/modules-macros-win32-<arch>` with the same flag. `getScannerBinaryPath()` in the TypeScript wrapper returns the binary for the current platform and architecture. On Windows it falls back to the local `npm run build` output in `platforms
|
|
105
|
+
On Windows and Linux, the compiler loads `ExpoModulesMacros.exe` from `@expo/modules-macros-win32-<arch>` or `ExpoModulesMacros` from `@expo/modules-macros-linux-<arch>` with the same flag. `getScannerBinaryPath()` in the TypeScript wrapper returns the binary for the current platform and architecture. On Windows and Linux it falls back to the local `npm run build` output in `platforms/<platform>-<arch>` when the platform package isn't installed, for example in this repository.
|
|
101
106
|
|
|
102
107
|
The module and type names in `#externalMacro` must stay in sync with `apple/Sources/ExpoModulesMacros/Plugin.swift`.
|
|
103
108
|
|
|
104
109
|
# Development
|
|
105
110
|
|
|
106
|
-
Requires a toolchain with Swift 6.2 or newer: on macOS 13 or newer that means Xcode 26 or newer, and on Windows the Swift toolchain from swift.org (CI uses 6.4).
|
|
111
|
+
Requires a toolchain with Swift 6.2 or newer: on macOS 13 or newer that means Xcode 26 or newer, and on Windows and Linux the Swift toolchain from swift.org (CI uses 6.4).
|
|
107
112
|
|
|
108
113
|
```sh
|
|
109
114
|
cd apple
|
|
@@ -115,16 +120,17 @@ swift test
|
|
|
115
120
|
|
|
116
121
|
- On macOS, it builds for arm64 and x86_64, merges the slices into `apple/ExpoModulesMacros` with `lipo`, strips it, and verifies both slices are present. SwiftPM only builds macro tools for the host architecture, so the x86_64 slice is produced by running the toolchain under Rosetta; the script installs Rosetta if it is missing.
|
|
117
122
|
- On Windows, it builds for the host architecture only, with the Swift runtime linked in statically and without debug info, and writes `platforms/win32-<arch>/ExpoModulesMacros.exe` (`x64` or `arm64`, as Node's `process.arch` names them), the file that the package for that architecture publishes. It strips the executable with `llvm-strip` from the Swift toolchain and checks the architecture in its header.
|
|
123
|
+
- On Linux, it also builds for the host architecture only, with the Swift runtime linked in statically, and writes `platforms/linux-<arch>/ExpoModulesMacros`. It strips the executable with `strip` and checks the architecture in its ELF header. The executable needs the glibc version of the machine it's built on or a newer one, so CI and the Publish workflow build it in the `swift:6.4.0-rhel-ubi9` image (Red Hat UBI 9, glibc 2.34).
|
|
118
124
|
|
|
119
|
-
The macOS binary is committed to the repository. The Windows binaries aren't: the Publish workflow builds them and puts them into the packages in `platforms/`.
|
|
125
|
+
The macOS binary is committed to the repository. The Windows and Linux binaries aren't: the Publish workflow builds them and puts them into the packages in `platforms/`.
|
|
120
126
|
|
|
121
127
|
# Releasing
|
|
122
128
|
|
|
123
129
|
The **Publish** workflow is manual (`workflow_dispatch`) and takes a release type. It bumps the version, builds the universal binary, and publishes to npm through OIDC trusted publishing. The commit, tag and GitHub release are created only after the publish succeeds, so a failed build leaves the branch untouched.
|
|
124
130
|
|
|
125
|
-
Windows jobs (x64 and arm64) build the
|
|
131
|
+
Windows and Linux jobs (x64 and arm64 for each) build their binaries first. Verify jobs then install each binary from its packed package and scan through the TypeScript wrapper with `scripts/check-scan.js`, on Windows runners without Swift and, for Linux, in the Ubuntu 22.04, Debian 12 and Amazon Linux 2023 images. The publish job does the same for the macOS binary, so a failed check stops the release before anything is published. After the version bump, `scripts/set-platform-versions.js` gives the packages in `platforms/` the same version and lists them in this package's `optionalDependencies` with that exact version. The workflow publishes the four platform packages before `expo-modules-macros`, so this package is never on npm without them. Each package needs trusted publishing configured on npmjs.com for the Publish workflow.
|
|
126
132
|
|
|
127
|
-
Prerelease versions (`prepatch`, `preminor`, `premajor`, `prerelease`) are published with the `next` dist-tag, and the others with `latest`. With **Dry run** checked, the workflow builds everything and runs `npm publish --dry-run` for the
|
|
133
|
+
Prerelease versions (`prepatch`, `preminor`, `premajor`, `prerelease`) are published with the `next` dist-tag, and the others with `latest`. With **Dry run** checked, the workflow builds everything and runs `npm publish --dry-run` for the five packages, but doesn't publish, commit, tag or create a release. Use it to check a change to the workflow.
|
|
128
134
|
|
|
129
135
|
# Contributing
|
|
130
136
|
|
package/apple/ExpoModulesMacros
CHANGED
|
Binary file
|
|
@@ -23,10 +23,13 @@ internal struct JSFunction {
|
|
|
23
23
|
let returnFreeForm: FreeFormConversion?
|
|
24
24
|
let isThrowing: Bool
|
|
25
25
|
let isAsync: Bool
|
|
26
|
+
/// The function's name in the source, where a JavaScript name collision is reported.
|
|
27
|
+
let nameToken: TokenSyntax
|
|
26
28
|
|
|
27
29
|
init(funcDecl: FunctionDeclSyntax, attribute: AttributeSyntax) {
|
|
28
30
|
self.swiftName = funcDecl.name.text
|
|
29
31
|
self.jsName = jsNameArgument(of: attribute) ?? withoutBackticks(funcDecl.name.text)
|
|
32
|
+
self.nameToken = funcDecl.name
|
|
30
33
|
self.parameters = Array(funcDecl.signature.parameterClause.parameters)
|
|
31
34
|
|
|
32
35
|
let declaredReturnType = funcDecl.signature.returnClause?.type
|
|
@@ -313,6 +316,8 @@ internal struct JSProperty {
|
|
|
313
316
|
/// Whether the property is settable from JS: `true` for a stored `var` or a computed `var` with an
|
|
314
317
|
/// explicit `set` accessor; `false` for a getter-only computed `var` or a `let`.
|
|
315
318
|
let isSettable: Bool
|
|
319
|
+
/// The property's name in the source, where a JavaScript name collision is reported.
|
|
320
|
+
let nameToken: TokenSyntax
|
|
316
321
|
|
|
317
322
|
/// The statements that install this property's accessor on the JS object, indented for the
|
|
318
323
|
/// decorator body. Builds a descriptor object (`enumerable` + `get`, and `set` when settable) via
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import SwiftDiagnostics
|
|
2
|
+
import SwiftSyntax
|
|
3
|
+
import SwiftSyntaxMacros
|
|
4
|
+
|
|
5
|
+
/// Reports each `@JS` member whose JavaScript name an earlier `@JS` member already binds on the same
|
|
6
|
+
/// JavaScript object. Pass the members of one object only: a shared object's static members bind onto
|
|
7
|
+
/// the constructor and its instance members onto the prototype, so the two sets are checked separately.
|
|
8
|
+
internal func diagnoseJSNameCollisions(
|
|
9
|
+
functions: [JSFunction],
|
|
10
|
+
properties: [JSProperty],
|
|
11
|
+
in context: some MacroExpansionContext
|
|
12
|
+
) {
|
|
13
|
+
// Each member binds with its own `setProperty` or `defineProperty` call, so of two members with one
|
|
14
|
+
// name, JavaScript would reach only one. Members are compared in source order, so the error lands on
|
|
15
|
+
// every member after the first, whichever kind each one is.
|
|
16
|
+
let members =
|
|
17
|
+
functions.map { BoundMember(jsName: $0.jsName, kind: "function", nameToken: $0.nameToken) }
|
|
18
|
+
+ properties.map { BoundMember(jsName: $0.jsName, kind: "property", nameToken: $0.nameToken) }
|
|
19
|
+
var firstMembers: [String: BoundMember] = [:]
|
|
20
|
+
|
|
21
|
+
for member in members.sorted(by: { $0.nameToken.position < $1.nameToken.position }) {
|
|
22
|
+
guard let first = firstMembers[member.jsName] else {
|
|
23
|
+
firstMembers[member.jsName] = member
|
|
24
|
+
continue
|
|
25
|
+
}
|
|
26
|
+
context.diagnose(
|
|
27
|
+
Diagnostic(
|
|
28
|
+
node: member.nameToken,
|
|
29
|
+
message: JSNameCollisionMessage(
|
|
30
|
+
"The JavaScript name '\(member.jsName)' is already bound by an earlier @JS \(first.kind). A JavaScript object holds one member per name, so JavaScript could reach only one of them. Give one of them another name with @JS(\"...\")."
|
|
31
|
+
),
|
|
32
|
+
notes: [
|
|
33
|
+
Note(
|
|
34
|
+
node: Syntax(first.nameToken),
|
|
35
|
+
message: JSNameCollisionNote("'\(member.jsName)' is first bound here"))
|
|
36
|
+
]
|
|
37
|
+
))
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
private struct BoundMember {
|
|
42
|
+
let jsName: String
|
|
43
|
+
let kind: String
|
|
44
|
+
let nameToken: TokenSyntax
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
private struct JSNameCollisionMessage: DiagnosticMessage {
|
|
48
|
+
let message: String
|
|
49
|
+
let diagnosticID = MessageID(domain: "ExpoModulesMacros", id: "js-name-collision")
|
|
50
|
+
let severity: DiagnosticSeverity = .error
|
|
51
|
+
|
|
52
|
+
init(_ message: String) {
|
|
53
|
+
self.message = message
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private struct JSNameCollisionNote: NoteMessage {
|
|
58
|
+
let message: String
|
|
59
|
+
let noteID = MessageID(domain: "ExpoModulesMacros", id: "js-name-collision-first")
|
|
60
|
+
|
|
61
|
+
init(_ message: String) {
|
|
62
|
+
self.message = message
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -441,7 +441,8 @@ internal func collectProperties(
|
|
|
441
441
|
valueType: valueType,
|
|
442
442
|
// A literal-inferred type never mentions `Any`, so only an explicit annotation can be free-form.
|
|
443
443
|
freeForm: binding.typeAnnotation.flatMap { freeFormConversion(of: $0.type) },
|
|
444
|
-
isSettable: isVar && bindingIsSettable(binding)
|
|
444
|
+
isSettable: isVar && bindingIsSettable(binding),
|
|
445
|
+
nameToken: ident.identifier
|
|
445
446
|
)
|
|
446
447
|
}
|
|
447
448
|
}
|
|
@@ -9,14 +9,16 @@ import SwiftSyntaxMacros
|
|
|
9
9
|
needed — and synthesizes the conversion surface from the property's static type:
|
|
10
10
|
|
|
11
11
|
- an explicit memberwise `init` (so the static factories have a single construction point),
|
|
12
|
-
- `
|
|
13
|
-
|
|
14
|
-
- `from(
|
|
15
|
-
- `
|
|
12
|
+
- `decode(_:in:)` and `encode(_:in:)` — the fast paths, reading each property straight off a JS
|
|
13
|
+
object and writing it straight into one,
|
|
14
|
+
- `from(object:appContext:)` and `toObject(appContext:)`, which forward to `decode` and `encode`,
|
|
15
|
+
- `from(dictionary:appContext:)` and `toDictionary(appContext:)` — converting each property from and
|
|
16
|
+
to a `[String: Any]` dictionary.
|
|
16
17
|
|
|
17
|
-
The type is auto-conformed to `Record` (a core protocol whose requirements are
|
|
18
|
-
|
|
19
|
-
shape — no conformance to
|
|
18
|
+
The type is auto-conformed to `Record` (a core protocol whose requirements are this surface); the
|
|
19
|
+
synthesized methods override `Record`'s reflection-based defaults, and the synthesized `decode` and
|
|
20
|
+
`encode` replace its default `JavaScriptCodable` witnesses. Author-facing shape — no conformance to
|
|
21
|
+
spell out:
|
|
20
22
|
|
|
21
23
|
@Record
|
|
22
24
|
struct Options {
|
|
@@ -31,7 +33,7 @@ import SwiftSyntaxMacros
|
|
|
31
33
|
default is required (the factories throw when the source omits it).
|
|
32
34
|
|
|
33
35
|
The JS-value paths convert through `JavaScriptDecodable.decode` / `JavaScriptEncodable.encode`
|
|
34
|
-
(`
|
|
36
|
+
(`decode(_:in:)` / `encode(_:in:)`); the native-`Any` dictionary paths still go through the
|
|
35
37
|
public dynamic-type API — `T.getDynamicType()` plus `cast(_:appContext:)` / `convertToJS(_:appContext:)`
|
|
36
38
|
(`from(dictionary:)` / `toDictionary(appContext:)`), since `JavaScriptCodable` only converts JS
|
|
37
39
|
values, not native `Any`. Both spell types as `public` symbols so the synthesized code compiles inside
|
|
@@ -55,10 +57,15 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
55
57
|
conformingTo protocols: [TypeSyntax],
|
|
56
58
|
in context: some MacroExpansionContext
|
|
57
59
|
) throws -> [DeclSyntax] {
|
|
58
|
-
let
|
|
59
|
-
|
|
60
|
+
let typeName: String
|
|
61
|
+
if let structDecl = declaration.as(StructDeclSyntax.self) {
|
|
62
|
+
typeName = structDecl.name.text
|
|
63
|
+
} else if let classDecl = declaration.as(ClassDeclSyntax.self) {
|
|
64
|
+
typeName = classDecl.name.text
|
|
65
|
+
} else {
|
|
60
66
|
throw MacroExpansionErrorMessage("@Record can only be applied to a struct or class")
|
|
61
67
|
}
|
|
68
|
+
let isClass = declaration.is(ClassDeclSyntax.self)
|
|
62
69
|
|
|
63
70
|
let properties = try recordProperties(of: declaration)
|
|
64
71
|
let inheritsRecord = isClass && classHasInheritance(declaration)
|
|
@@ -99,10 +106,13 @@ public struct RecordMacro: MemberMacro, ExtensionMacro {
|
|
|
99
106
|
if !existingInitLabels.contains(properties.map { $0.key }) {
|
|
100
107
|
members.append(memberwiseInit(properties: properties))
|
|
101
108
|
}
|
|
102
|
-
members.append(fromJSObjectFactory(properties: properties))
|
|
109
|
+
members.append(fromJSObjectFactory(properties: properties, inheritsRecord: inheritsRecord))
|
|
103
110
|
members.append(fromDictionaryFactory(properties: properties))
|
|
104
111
|
members.append(toDictionaryMethod(properties: properties, inheritsRecord: inheritsRecord))
|
|
105
112
|
members.append(toObjectMethod(properties: properties, inheritsRecord: inheritsRecord))
|
|
113
|
+
if !inheritsRecord {
|
|
114
|
+
members.append(contentsOf: javaScriptCodableWitnesses(properties: properties, typeName: typeName))
|
|
115
|
+
}
|
|
106
116
|
return members
|
|
107
117
|
}
|
|
108
118
|
|
|
@@ -326,15 +336,24 @@ private func memberwiseInit(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
326
336
|
}
|
|
327
337
|
|
|
328
338
|
/**
|
|
329
|
-
`from(object:appContext:)
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
undefined; optional properties become `nil` when undefined/null. Each property is decoded with
|
|
333
|
-
`JavaScriptDecodable.decode`, so the factory binds the `runtime` from the app context once and
|
|
334
|
-
threads it to every read. A free-form property decodes through the `JavaScriptValue` free-form
|
|
335
|
-
methods, which take the runtime too.
|
|
339
|
+
`from(object:appContext:)`. A record forwards to its own unowned `decode(_:in:)` witness (see
|
|
340
|
+
`javaScriptCodableWitnesses`), which holds the property reads. A subclass, which gets no witnesses,
|
|
341
|
+
reads the properties here, in the app context's runtime.
|
|
336
342
|
*/
|
|
337
|
-
private func fromJSObjectFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
343
|
+
private func fromJSObjectFactory(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
344
|
+
guard inheritsRecord else {
|
|
345
|
+
// The record declares only the unowned `decode`, so the call lends the value unowned. With an owning
|
|
346
|
+
// value it would reach an owning `decode` default, and `Record`'s converts through this factory.
|
|
347
|
+
return """
|
|
348
|
+
@JavaScriptActor
|
|
349
|
+
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
350
|
+
let runtime = try appContext.runtime
|
|
351
|
+
return try object.asValue().withUnownedValue(in: runtime) { unownedValue in
|
|
352
|
+
return try decode(unownedValue, in: runtime)
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
"""
|
|
356
|
+
}
|
|
338
357
|
var lines: [String] = []
|
|
339
358
|
// Only bind the runtime when there's a property to decode — an empty record's factory would
|
|
340
359
|
// otherwise leave it unused.
|
|
@@ -342,11 +361,10 @@ private func fromJSObjectFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
342
361
|
lines.append(" let runtime = try appContext.runtime")
|
|
343
362
|
}
|
|
344
363
|
lines.append(factoryBody(properties: properties, readLines: jsObjectReadLines(properties: properties)))
|
|
345
|
-
let body = lines.joined(separator: "\n")
|
|
346
364
|
return """
|
|
347
365
|
@JavaScriptActor
|
|
348
366
|
public static func from(object: borrowing JavaScriptObject, appContext: AppContext) throws -> Self {
|
|
349
|
-
\(raw:
|
|
367
|
+
\(raw: lines.joined(separator: "\n"))
|
|
350
368
|
}
|
|
351
369
|
"""
|
|
352
370
|
}
|
|
@@ -373,69 +391,68 @@ private func fromDictionaryFactory(properties: [RecordProperty]) -> DeclSyntax {
|
|
|
373
391
|
private func factoryBody(properties: [RecordProperty], readLines: [String]) -> String {
|
|
374
392
|
var lines: [String] = []
|
|
375
393
|
lines.append(contentsOf: readLines)
|
|
376
|
-
let initArgs = properties.map { "\($
|
|
394
|
+
let initArgs = properties.enumerated().map { "\($1.name): \(fieldLocal($0))" }.joined(separator: ", ")
|
|
377
395
|
lines.append(" return Self(\(initArgs))")
|
|
378
396
|
return lines.joined(separator: "\n")
|
|
379
397
|
}
|
|
380
398
|
|
|
381
|
-
///
|
|
382
|
-
/// the
|
|
383
|
-
///
|
|
384
|
-
|
|
399
|
+
/// The local that holds the value of the property at `index` in a factory. It's numbered rather than
|
|
400
|
+
/// named after the property, so no property can collide with it or with the factory's other parameters
|
|
401
|
+
/// and locals (`object`, `runtime`, `dictionary`, `appContext`).
|
|
402
|
+
private func fieldLocal(_ index: Int) -> String {
|
|
403
|
+
return "field\(index)"
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/// Per-property read statements, each producing a `let field<index>` from the property of the JS object
|
|
407
|
+
/// named `object`. The property is lent to a closure as a `JavaScriptUnownedValue` and decoded there the
|
|
408
|
+
/// same way as a `@JS` argument (see `decodeCall`), so the read doesn't allocate a `JavaScriptValue`.
|
|
409
|
+
/// Only the generic free-form decode copies the value into one.
|
|
385
410
|
private func jsObjectReadLines(properties: [RecordProperty]) -> [String] {
|
|
386
411
|
var lines: [String] = []
|
|
387
|
-
for property in properties {
|
|
412
|
+
for (index, property) in properties.enumerated() {
|
|
388
413
|
let valueVar = derivedIdentifier(from: property.name, suffix: "JSValue")
|
|
389
|
-
let
|
|
390
|
-
let
|
|
391
|
-
switch property.freeForm {
|
|
392
|
-
case .shape(let decodeMethod, _, let isOptional, _):
|
|
393
|
-
let call = "try JavaScriptValue.\(decodeMethod)(\(valueVar), in: runtime)"
|
|
394
|
-
decode = isOptional ? "\(valueVar).isUndefined() || \(valueVar).isNull() ? nil : \(call)" : call
|
|
395
|
-
case .generic:
|
|
396
|
-
decode = "try JavaScriptValue.decodeAny(\(valueVar), as: \(exprType).self, in: runtime)"
|
|
397
|
-
case nil:
|
|
398
|
-
decode = "try \(exprType).decode(\(valueVar), in: runtime)"
|
|
399
|
-
}
|
|
400
|
-
lines.append(" let \(valueVar) = object.getProperty(\"\(property.key)\")")
|
|
414
|
+
let decode = "try \(decodeCall(property.type, freeForm: property.freeForm, from: valueVar))"
|
|
415
|
+
lines.append(" let \(fieldLocal(index)) = try object.withUnownedProperty(\"\(property.key)\") { \(valueVar) in")
|
|
401
416
|
if property.isRequired {
|
|
402
|
-
lines.append("
|
|
403
|
-
lines.append("
|
|
404
|
-
lines.append("
|
|
405
|
-
lines.append("
|
|
417
|
+
lines.append(" guard !\(valueVar).isUndefined() else {")
|
|
418
|
+
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
419
|
+
lines.append(" }")
|
|
420
|
+
lines.append(" return \(decode)")
|
|
406
421
|
} else if property.isOptional {
|
|
407
|
-
// `Optional.decode` (and the free-form
|
|
408
|
-
// the read is a plain decode.
|
|
409
|
-
lines.append("
|
|
422
|
+
// `Optional.decode` (and the free-form decodes of optional shapes) already map `undefined` and
|
|
423
|
+
// `null` to `nil`, so the read is a plain decode.
|
|
424
|
+
lines.append(" return \(decode)")
|
|
410
425
|
} else {
|
|
411
|
-
lines.append("
|
|
426
|
+
lines.append(" return \(valueVar).isUndefined() ? \(property.defaultValue!) : \(decode)")
|
|
412
427
|
}
|
|
428
|
+
lines.append(" }")
|
|
413
429
|
}
|
|
414
430
|
return lines
|
|
415
431
|
}
|
|
416
432
|
|
|
417
|
-
/// Per-property read statements for the dictionary factory, each producing a `let <
|
|
418
|
-
/// property has no dynamic type to cast through, and
|
|
419
|
-
/// read unchanged.
|
|
433
|
+
/// Per-property read statements for the dictionary factory, each producing a `let field<index>` from the
|
|
434
|
+
/// raw dictionary value in `entry<index>`. A bare `Any` property has no dynamic type to cast through, and
|
|
435
|
+
/// the dictionary value already is an `Any`, so it's read unchanged.
|
|
420
436
|
private func dictionaryReadLines(properties: [RecordProperty]) -> [String] {
|
|
421
437
|
var lines: [String] = []
|
|
422
|
-
for property in properties {
|
|
423
|
-
let
|
|
438
|
+
for (index, property) in properties.enumerated() {
|
|
439
|
+
let field = fieldLocal(index)
|
|
440
|
+
let entry = "entry\(index)"
|
|
424
441
|
let exprType = expressionType(property.type)
|
|
425
442
|
let isBareAny = property.freeForm?.isBareAny == true
|
|
426
443
|
let cast = isBareAny
|
|
427
|
-
?
|
|
428
|
-
: "try \(exprType).getDynamicType().cast(\(
|
|
429
|
-
lines.append(" let \(
|
|
444
|
+
? entry
|
|
445
|
+
: "try \(exprType).getDynamicType().cast(\(entry), appContext: appContext) as! \(exprType)"
|
|
446
|
+
lines.append(" let \(entry) = dictionary[\"\(property.key)\"]")
|
|
430
447
|
if property.isRequired {
|
|
431
|
-
lines.append(" guard let \(
|
|
448
|
+
lines.append(" guard let \(entry) else {")
|
|
432
449
|
lines.append(" throw RecordPropertyRequiredException(\"\(property.key)\")")
|
|
433
450
|
lines.append(" }")
|
|
434
|
-
lines.append(" let \(
|
|
451
|
+
lines.append(" let \(field) = \(cast)")
|
|
435
452
|
} else if property.isOptional {
|
|
436
|
-
lines.append(" let \(
|
|
453
|
+
lines.append(" let \(field): \(property.type) = (\(entry) == nil || \(entry)! is NSNull) ? nil : \(cast)")
|
|
437
454
|
} else {
|
|
438
|
-
lines.append(" let \(
|
|
455
|
+
lines.append(" let \(field) = \(entry) == nil ? \(property.defaultValue!) : \(isBareAny ? entry + "!" : cast)")
|
|
439
456
|
}
|
|
440
457
|
}
|
|
441
458
|
return lines
|
|
@@ -474,51 +491,94 @@ private func toDictionaryMethod(properties: [RecordProperty], inheritsRecord: Bo
|
|
|
474
491
|
}
|
|
475
492
|
|
|
476
493
|
/**
|
|
477
|
-
`toObject(appContext:)
|
|
478
|
-
`
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
properties are written first.
|
|
494
|
+
`toObject(appContext:)`. A record forwards to its own `encode(_:in:)` witness (see
|
|
495
|
+
`javaScriptCodableWitnesses`), which holds the property writes. A subclass, which gets no witnesses,
|
|
496
|
+
overrides it to chain to `super`, so inherited properties are written first, and writes its own
|
|
497
|
+
properties in the app context's runtime.
|
|
482
498
|
*/
|
|
483
499
|
private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool) -> DeclSyntax {
|
|
484
|
-
|
|
500
|
+
guard inheritsRecord else {
|
|
501
|
+
return """
|
|
502
|
+
@JavaScriptActor
|
|
503
|
+
public func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
504
|
+
return try Self.encode(self, in: appContext.runtime).asObject()
|
|
505
|
+
}
|
|
506
|
+
"""
|
|
507
|
+
}
|
|
485
508
|
var lines: [String] = []
|
|
486
|
-
// The
|
|
487
|
-
//
|
|
488
|
-
|
|
489
|
-
if !inheritsRecord || !properties.isEmpty {
|
|
509
|
+
// The runtime is only needed when there's a property to encode (the object itself comes from
|
|
510
|
+
// `super`). Binding it when unused would warn.
|
|
511
|
+
if !properties.isEmpty {
|
|
490
512
|
lines.append(" let runtime = try appContext.runtime")
|
|
491
513
|
}
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
514
|
+
lines.append(" let object = try super.toObject(appContext: appContext)")
|
|
515
|
+
lines.append(contentsOf: objectWriteLines(properties: properties, of: "self"))
|
|
516
|
+
lines.append(" return object")
|
|
517
|
+
return """
|
|
518
|
+
@JavaScriptActor
|
|
519
|
+
public override func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
520
|
+
\(raw: lines.joined(separator: "\n"))
|
|
521
|
+
}
|
|
522
|
+
"""
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
/**
|
|
526
|
+
The record's own `JavaScriptDecodable` and `JavaScriptEncodable` witnesses, which convert in the
|
|
527
|
+
runtime they're given. Without them the record would convert through `Record`'s default witnesses,
|
|
528
|
+
which recover the app context from the runtime and convert in the app context's runtime.
|
|
529
|
+
|
|
530
|
+
`decode` reads each property off the JS object into a local, then constructs the record through the
|
|
531
|
+
memberwise init. Required properties throw when undefined; defaulted properties fall back to the
|
|
532
|
+
property's declared default (inlined) when undefined; optional properties become `nil` when
|
|
533
|
+
undefined/null. It's the unowned overload: an owning value decodes through `JavaScriptDecodable`'s
|
|
534
|
+
default, which forwards to it. `encode` builds the JS object, writing each property. Its parameter
|
|
535
|
+
names the type, because a class can't take `Self` as a parameter type.
|
|
536
|
+
*/
|
|
537
|
+
private func javaScriptCodableWitnesses(properties: [RecordProperty], typeName: String) -> [DeclSyntax] {
|
|
538
|
+
// An empty record still decodes only from an object, but has no property to read off it.
|
|
539
|
+
let objectLine = properties.isEmpty
|
|
540
|
+
? " _ = try value.asObject(in: runtime)"
|
|
541
|
+
: " let object = try value.asObject(in: runtime)"
|
|
542
|
+
let decodeBody = objectLine + "\n" + factoryBody(properties: properties, readLines: jsObjectReadLines(properties: properties))
|
|
543
|
+
let encodeLines = [" let object = runtime.createObject()"]
|
|
544
|
+
+ objectWriteLines(properties: properties, of: "value")
|
|
545
|
+
+ [" return object.asValue()"]
|
|
546
|
+
return [
|
|
547
|
+
"""
|
|
548
|
+
@JavaScriptActor
|
|
549
|
+
public static func decode(_ value: borrowing JavaScriptUnownedValue, in runtime: borrowing JavaScriptRuntime) throws -> Self {
|
|
550
|
+
\(raw: decodeBody)
|
|
551
|
+
}
|
|
552
|
+
""",
|
|
553
|
+
"""
|
|
554
|
+
@JavaScriptActor
|
|
555
|
+
public static func encode(_ value: \(raw: typeName), in runtime: borrowing JavaScriptRuntime) throws -> JavaScriptValue {
|
|
556
|
+
\(raw: encodeLines.joined(separator: "\n"))
|
|
557
|
+
}
|
|
558
|
+
""",
|
|
559
|
+
]
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/// The statements that write each property of `record` (`self` or the `encode` parameter) into the
|
|
563
|
+
/// `object` local, encoding it with `JavaScriptEncodable.encode` or, for a free-form property, the
|
|
564
|
+
/// matching `JavaScriptValue` free-form method.
|
|
565
|
+
private func objectWriteLines(properties: [RecordProperty], of record: String) -> [String] {
|
|
566
|
+
return properties.map { property in
|
|
567
|
+
let value = "\(record).\(property.name)"
|
|
499
568
|
let encode: String
|
|
500
569
|
switch property.freeForm {
|
|
501
570
|
case .shape(_, let encodeMethod, let isOptional, _):
|
|
502
571
|
encode = isOptional
|
|
503
|
-
? "
|
|
504
|
-
: "try JavaScriptValue.\(encodeMethod)(
|
|
572
|
+
? "\(value) == nil ? .null() : try JavaScriptValue.\(encodeMethod)(\(value)!, in: runtime)"
|
|
573
|
+
: "try JavaScriptValue.\(encodeMethod)(\(value), in: runtime)"
|
|
505
574
|
case .generic:
|
|
506
575
|
// The explicit `as Any` keeps an optional property from warning about its implicit coercion.
|
|
507
|
-
encode = "try JavaScriptValue.encodeAny(
|
|
576
|
+
encode = "try JavaScriptValue.encodeAny(\(value) as Any, in: runtime)"
|
|
508
577
|
case nil:
|
|
509
|
-
encode = "try \(
|
|
578
|
+
encode = "try \(expressionType(property.type)).encode(\(value), in: runtime)"
|
|
510
579
|
}
|
|
511
|
-
|
|
580
|
+
return " object.setProperty(\"\(property.key)\", value: \(encode))"
|
|
512
581
|
}
|
|
513
|
-
lines.append(" return object")
|
|
514
|
-
let body = lines.joined(separator: "\n")
|
|
515
|
-
|
|
516
|
-
return """
|
|
517
|
-
@JavaScriptActor
|
|
518
|
-
public \(raw: overrideKeyword)func toObject(appContext: AppContext) throws -> JavaScriptObject {
|
|
519
|
-
\(raw: body)
|
|
520
|
-
}
|
|
521
|
-
"""
|
|
522
582
|
}
|
|
523
583
|
|
|
524
584
|
// MARK: - Helpers
|
|
@@ -93,6 +93,9 @@ public struct SharedObjectMacro: MemberMacro {
|
|
|
93
93
|
}
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
diagnoseJSNameCollisions(functions: instanceFunctions, properties: instanceProperties, in: context)
|
|
97
|
+
diagnoseJSNameCollisions(functions: staticFunctions, properties: staticProperties, in: context)
|
|
98
|
+
|
|
96
99
|
let lines = entries.map { " \($0)" }.joined(separator: "\n")
|
|
97
100
|
let body = entries.isEmpty
|
|
98
101
|
? " return Class(\"\(jsName)\", \(typeName).self) {\n }"
|
package/build/index.d.ts
CHANGED
|
@@ -41,9 +41,10 @@ export declare class ScannerSchemaVersionError extends Error {
|
|
|
41
41
|
*
|
|
42
42
|
* - macOS: the universal binary shipped in this package, next to the Swift package rather than in a
|
|
43
43
|
* `bin` directory.
|
|
44
|
-
* - Windows: `ExpoModulesMacros.exe` from the package for the
|
|
45
|
-
* `
|
|
46
|
-
* it falls back to the executable that `npm run build`
|
|
44
|
+
* - Windows and Linux: `ExpoModulesMacros.exe` or `ExpoModulesMacros` from the package for the
|
|
45
|
+
* current platform and architecture (see `platformBinaryPackageName`). When that package isn't
|
|
46
|
+
* installed, for example in this repository, it falls back to the executable that `npm run build`
|
|
47
|
+
* writes to `platforms/<platform>-<arch>`.
|
|
47
48
|
*
|
|
48
49
|
* The path isn't checked here; running a missing binary fails with a `ScannerError` that says how to
|
|
49
50
|
* get it.
|
package/build/index.js
CHANGED
|
@@ -51,12 +51,21 @@ class ScannerSchemaVersionError extends Error {
|
|
|
51
51
|
}
|
|
52
52
|
exports.ScannerSchemaVersionError = ScannerSchemaVersionError;
|
|
53
53
|
/**
|
|
54
|
-
* The
|
|
55
|
-
*
|
|
56
|
-
* `cpu` fields make package managers install it only on a matching Windows machine.
|
|
54
|
+
* The platforms whose binaries ship in packages of their own, one per architecture, with the
|
|
55
|
+
* machines that those packages install on.
|
|
57
56
|
*/
|
|
58
|
-
|
|
59
|
-
|
|
57
|
+
const platformPackageMachines = {
|
|
58
|
+
win32: 'Windows x64 and arm64',
|
|
59
|
+
linux: 'Linux x64 and arm64 with glibc',
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The package with the binary for the current platform and architecture, for example
|
|
63
|
+
* `@expo/modules-macros-win32-x64` or `@expo/modules-macros-linux-arm64`. It is an optional
|
|
64
|
+
* dependency of this package, and its `os`, `cpu` and (on Linux) `libc` fields make package managers
|
|
65
|
+
* install it only on a matching machine.
|
|
66
|
+
*/
|
|
67
|
+
function platformBinaryPackageName() {
|
|
68
|
+
return `@expo/modules-macros-${process.platform}-${process.arch}`;
|
|
60
69
|
}
|
|
61
70
|
/**
|
|
62
71
|
* Absolute path to the scanner binary for the current platform. It doubles as the macro plugin
|
|
@@ -64,34 +73,40 @@ function windowsBinaryPackageName() {
|
|
|
64
73
|
*
|
|
65
74
|
* - macOS: the universal binary shipped in this package, next to the Swift package rather than in a
|
|
66
75
|
* `bin` directory.
|
|
67
|
-
* - Windows: `ExpoModulesMacros.exe` from the package for the
|
|
68
|
-
* `
|
|
69
|
-
* it falls back to the executable that `npm run build`
|
|
76
|
+
* - Windows and Linux: `ExpoModulesMacros.exe` or `ExpoModulesMacros` from the package for the
|
|
77
|
+
* current platform and architecture (see `platformBinaryPackageName`). When that package isn't
|
|
78
|
+
* installed, for example in this repository, it falls back to the executable that `npm run build`
|
|
79
|
+
* writes to `platforms/<platform>-<arch>`.
|
|
70
80
|
*
|
|
71
81
|
* The path isn't checked here; running a missing binary fails with a `ScannerError` that says how to
|
|
72
82
|
* get it.
|
|
73
83
|
*/
|
|
74
84
|
function getScannerBinaryPath() {
|
|
75
|
-
if (process.platform
|
|
85
|
+
if (process.platform === 'darwin') {
|
|
76
86
|
return node_path_1.default.join(__dirname, '..', 'apple', 'ExpoModulesMacros');
|
|
77
87
|
}
|
|
88
|
+
const fileName = process.platform === 'win32' ? 'ExpoModulesMacros.exe' : 'ExpoModulesMacros';
|
|
78
89
|
try {
|
|
79
|
-
const packageJson = require.resolve(`${
|
|
80
|
-
return node_path_1.default.join(node_path_1.default.dirname(packageJson),
|
|
90
|
+
const packageJson = require.resolve(`${platformBinaryPackageName()}/package.json`);
|
|
91
|
+
return node_path_1.default.join(node_path_1.default.dirname(packageJson), fileName);
|
|
81
92
|
}
|
|
82
93
|
catch {
|
|
83
|
-
return node_path_1.default.join(__dirname, '..', 'platforms',
|
|
94
|
+
return node_path_1.default.join(__dirname, '..', 'platforms', `${process.platform}-${process.arch}`, fileName);
|
|
84
95
|
}
|
|
85
96
|
}
|
|
86
97
|
/** The message for a scanner binary that doesn't exist at `binaryPath`. */
|
|
87
98
|
function missingBinaryMessage(binaryPath) {
|
|
88
|
-
if (process.platform === '
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
99
|
+
if (process.platform === 'darwin') {
|
|
100
|
+
return `The scanner binary is missing at ${binaryPath}. Run \`npm run build\` in this package to build it.`;
|
|
101
|
+
}
|
|
102
|
+
const machines = platformPackageMachines[process.platform];
|
|
103
|
+
if (machines === undefined) {
|
|
104
|
+
return `The scanner binary is missing at ${binaryPath}. No binary is built for ${process.platform}.`;
|
|
93
105
|
}
|
|
94
|
-
return `The scanner binary is missing at ${binaryPath}.
|
|
106
|
+
return (`The scanner binary is missing at ${binaryPath}. It comes from the optional package ` +
|
|
107
|
+
`${platformBinaryPackageName()}, which your package manager installs only on ${machines}. ` +
|
|
108
|
+
'Reinstall dependencies without skipping optional ones (for example without `--omit=optional`), ' +
|
|
109
|
+
'or run `npm run build` in this package to build it locally.');
|
|
95
110
|
}
|
|
96
111
|
/**
|
|
97
112
|
* Runs a scanner subcommand and parses its JSON report.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "expo-modules-macros",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.21.0",
|
|
4
4
|
"description": "Swift macro plugin for Expo modules",
|
|
5
5
|
"main": "build/index.js",
|
|
6
6
|
"types": "build/index.d.ts",
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
"typescript": "^5.4.0"
|
|
24
24
|
},
|
|
25
25
|
"optionalDependencies": {
|
|
26
|
-
"@expo/modules-macros-
|
|
27
|
-
"@expo/modules-macros-
|
|
26
|
+
"@expo/modules-macros-linux-arm64": "0.21.0",
|
|
27
|
+
"@expo/modules-macros-linux-x64": "0.21.0",
|
|
28
|
+
"@expo/modules-macros-win32-arm64": "0.21.0",
|
|
29
|
+
"@expo/modules-macros-win32-x64": "0.21.0"
|
|
28
30
|
}
|
|
29
31
|
}
|