expo-modules-macros 0.21.0 → 0.23.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.
Files changed (41) hide show
  1. package/.github/workflows/linux.yml +8 -5
  2. package/.github/workflows/publish.yml +26 -11
  3. package/.github/workflows/swift-format.yml +79 -0
  4. package/.github/workflows/swift.yml +7 -4
  5. package/.github/workflows/windows.yml +8 -5
  6. package/AGENTS.md +16 -9
  7. package/README.md +20 -5
  8. package/apple/ExpoModulesMacros +0 -0
  9. package/apple/Package.resolved +3 -3
  10. package/apple/Package.swift +1 -2
  11. package/apple/Sources/ExpoModulesMacros/ClosureArguments.swift +39 -14
  12. package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +144 -30
  13. package/apple/Sources/ExpoModulesMacros/EventMacro.swift +29 -15
  14. package/apple/Sources/ExpoModulesMacros/ExpoModuleMacro.swift +63 -68
  15. package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +2 -1
  16. package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +2 -61
  17. package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +6 -3
  18. package/apple/Sources/ExpoModulesMacros/JSMacro.swift +184 -48
  19. package/apple/Sources/ExpoModulesMacros/JSNameCollisions.swift +26 -6
  20. package/apple/Sources/ExpoModulesMacros/MacroHelpers.swift +172 -43
  21. package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +286 -222
  22. package/apple/Sources/ExpoModulesMacros/SharedObjectMacro.swift +49 -56
  23. package/apple/Sources/ExpoModulesMacros/TypeConformanceAssertion.swift +57 -27
  24. package/apple/Sources/ExpoModulesMacros/UnionMacro.swift +23 -9
  25. package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +10 -5
  26. package/apple/Sources/ExpoModulesScanner/CLI.swift +3 -1
  27. package/apple/Sources/ExpoModulesScanner/Core/DetectionVisitor.swift +11 -7
  28. package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +2 -1
  29. package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +10 -7
  30. package/apple/Sources/ExpoModulesScanner/Exports/Availability.swift +6 -3
  31. package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +20 -4
  32. package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +18 -5
  33. package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +8 -3
  34. package/apple/Sources/ExpoModulesScanner/Exports/SurfaceVisitor.swift +179 -46
  35. package/apple/Sources/ExpoModulesScanner/Exports/TypeNode.swift +28 -8
  36. package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +8 -2
  37. package/build/index.d.ts +1 -1
  38. package/build/index.js +3 -3
  39. package/build/types.d.ts +32 -2
  40. package/build/types.js +1 -1
  41. package/package.json +8 -5
@@ -50,6 +50,9 @@ jobs:
50
50
  with:
51
51
  node-version: 24
52
52
 
53
+ - name: Setup pnpm
54
+ uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
55
+
53
56
  - name: Cache SwiftPM build
54
57
  uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
55
58
  with:
@@ -65,10 +68,10 @@ jobs:
65
68
  run: swift test
66
69
 
67
70
  - name: Build release tool
68
- # Runs `npm run build` (scripts/build.js): builds the release plugin binary for this
71
+ # Runs `pnpm run build` (scripts/build.js): builds the release plugin binary for this
69
72
  # architecture as platforms/linux-<arch>/ExpoModulesMacros.
70
73
  working-directory: ${{ github.workspace }}
71
- run: npm run build
74
+ run: pnpm run build
72
75
 
73
76
  - name: Verify the release tool
74
77
  # With no arguments it is the plugin server, which exits when stdin closes; with arguments it
@@ -125,8 +128,8 @@ jobs:
125
128
  # `@expo/modules-macros-linux-<arch>`, as it would in a project, not the local build.
126
129
  working-directory: ${{ github.workspace }}
127
130
  run: |
128
- npm install
129
- npm install --no-save "$PLATFORM_PACKAGE_TARBALL"
130
- npm run build:ts
131
+ pnpm install
132
+ pnpm add --save-optional "$PLATFORM_PACKAGE_TARBALL"
133
+ pnpm run build:ts
131
134
  node scripts/check-scan.js .
132
135
 
@@ -56,10 +56,13 @@ jobs:
56
56
  with:
57
57
  node-version: 24
58
58
 
59
+ - name: Setup pnpm
60
+ uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
61
+
59
62
  - name: Build release tool
60
63
  # Runs scripts/build.js: builds the release plugin binary for this architecture as
61
64
  # platforms/win32-<arch>/ExpoModulesMacros.exe.
62
- run: npm run build
65
+ run: pnpm run build
63
66
 
64
67
  - name: Upload tool
65
68
  uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
@@ -94,10 +97,13 @@ jobs:
94
97
  with:
95
98
  node-version: 24
96
99
 
100
+ - name: Setup pnpm
101
+ uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
102
+
97
103
  - name: Build release tool
98
104
  # Runs scripts/build.js: builds the release plugin binary for this architecture as
99
105
  # platforms/linux-<arch>/ExpoModulesMacros.
100
- run: npm run build
106
+ run: pnpm run build
101
107
 
102
108
  - name: Upload tool
103
109
  uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
@@ -130,6 +136,9 @@ jobs:
130
136
  with:
131
137
  node-version: 24
132
138
 
139
+ - name: Setup pnpm
140
+ uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
141
+
133
142
  - name: Download tool
134
143
  uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
135
144
  with:
@@ -142,11 +151,11 @@ jobs:
142
151
  # The `./` matters: npm reads `platforms/win32-<arch>` as a GitHub repository.
143
152
  $pack = npm pack "./platforms/win32-${{ matrix.arch }}" --pack-destination $env:RUNNER_TEMP --json | ConvertFrom-Json
144
153
  if ($LASTEXITCODE -ne 0) { throw "npm pack failed" }
145
- npm install
154
+ pnpm install
146
155
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
147
- npm install --no-save (Join-Path $env:RUNNER_TEMP $pack[0].filename)
156
+ pnpm add --save-optional (Join-Path $env:RUNNER_TEMP $pack[0].filename)
148
157
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
149
- npm run build:ts
158
+ pnpm run build:ts
150
159
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
151
160
  node scripts/check-scan.js .
152
161
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
@@ -178,6 +187,9 @@ jobs:
178
187
  with:
179
188
  node-version: 24
180
189
 
190
+ - name: Setup pnpm
191
+ uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
192
+
181
193
  - name: Download tool
182
194
  uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
183
195
  with:
@@ -191,9 +203,9 @@ jobs:
191
203
  # The `./` matters: npm reads `platforms/linux-<arch>` as a GitHub repository.
192
204
  npm pack "./platforms/linux-${{ matrix.arch }}" --pack-destination "$RUNNER_TEMP" --json > "$RUNNER_TEMP/pack.json"
193
205
  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
206
+ pnpm install
207
+ pnpm add --save-optional "$RUNNER_TEMP/$tarball"
208
+ pnpm run build:ts
197
209
 
198
210
  - name: Scan on each distribution
199
211
  run: |
@@ -233,6 +245,9 @@ jobs:
233
245
  node-version: 24
234
246
  registry-url: https://registry.npmjs.org
235
247
 
248
+ - name: Setup pnpm
249
+ uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
250
+
236
251
  - name: Update npm
237
252
  # Trusted publishing requires npm >= 11.5.1.
238
253
  run: npm install -g npm@latest
@@ -280,18 +295,18 @@ jobs:
280
295
 
281
296
  - name: Install dependencies
282
297
  # The wrapper's `prepublishOnly` build needs `tsc`; the tool binary needs none.
283
- run: npm install
298
+ run: pnpm install
284
299
 
285
300
  - name: Build release tool
286
301
  # Runs scripts/build.js: builds the release plugin binary,
287
302
  # copies it to apple/ExpoModulesMacros, and strips it.
288
- run: npm run build
303
+ run: pnpm run build
289
304
 
290
305
  - name: Verify the macOS tool
291
306
  # Scans through the TypeScript wrapper, which must find the binary in `apple/`, before
292
307
  # anything is published. The verify-windows and verify-linux jobs check the other binaries.
293
308
  run: |
294
- npm run build:ts
309
+ pnpm run build:ts
295
310
  node scripts/check-scan.js .
296
311
 
297
312
  - name: Download Windows and Linux tools
@@ -0,0 +1,79 @@
1
+ name: Swift Format
2
+
3
+ # Checks that the Swift sources are formatted with the pinned swift-format version.
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
+ shell: bash
19
+
20
+ env:
21
+ # Keep in sync with the version in the Development section of README.md.
22
+ SWIFT_FORMAT_VERSION: '604.0.0'
23
+
24
+ jobs:
25
+ lint:
26
+ name: Lint
27
+ runs-on: ubuntu-24.04
28
+ # The same image as the Linux workflow. swift-format links the Swift runtime from it.
29
+ container: swift:6.4.0-rhel-ubi9@sha256:802a003f1eacbca9c20664a8ba79621d13f4538c3d329e745b7f8d53c70604a3
30
+ timeout-minutes: 30
31
+ steps:
32
+ - name: Checkout
33
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
34
+
35
+ - name: Setup Node
36
+ uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
37
+ with:
38
+ node-version: 24
39
+
40
+ - name: Setup pnpm
41
+ uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
42
+
43
+ - name: Restore swift-format
44
+ id: swift-format-cache
45
+ uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
46
+ with:
47
+ path: ~/.local/bin/swift-format
48
+ # The binary links the Swift runtime of the container image, so the key changes with this
49
+ # file, which sets the image.
50
+ key: swift-format-${{ runner.os }}-${{ env.SWIFT_FORMAT_VERSION }}-${{ hashFiles('.github/workflows/swift-format.yml') }}
51
+
52
+ - name: Build swift-format
53
+ if: steps.swift-format-cache.outputs.cache-hit != 'true'
54
+ run: |
55
+ set -euo pipefail
56
+ src="$(mktemp -d)"
57
+ git clone --depth 1 --branch "$SWIFT_FORMAT_VERSION" https://github.com/swiftlang/swift-format.git "$src"
58
+ cd "$src"
59
+ swift build -c release --product swift-format
60
+ mkdir -p ~/.local/bin
61
+ cp "$(swift build -c release --show-bin-path)/swift-format" ~/.local/bin/swift-format
62
+
63
+ - name: Save swift-format
64
+ # Saves the binary before the lint, so that a run with findings doesn't build it again.
65
+ if: steps.swift-format-cache.outputs.cache-hit != 'true'
66
+ uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
67
+ with:
68
+ path: ~/.local/bin/swift-format
69
+ key: ${{ steps.swift-format-cache.outputs.cache-primary-key }}
70
+
71
+ - name: Add swift-format to PATH
72
+ run: echo "$HOME/.local/bin" >> "$GITHUB_PATH"
73
+
74
+ - name: Lint
75
+ run: |
76
+ # The script lists the files with `git ls-files`. The checkout belongs to another user than
77
+ # the container's root user, so Git needs it marked as safe.
78
+ git config --global --add safe.directory "$GITHUB_WORKSPACE"
79
+ pnpm run swift:lint
@@ -24,6 +24,9 @@ jobs:
24
24
  - name: Checkout
25
25
  uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
26
26
 
27
+ - name: Setup pnpm
28
+ uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
29
+
27
30
  - name: Select Xcode
28
31
  # Pin to an Xcode that ships Swift 6.2 (required by Package.swift).
29
32
  run: sudo xcode-select -switch /Applications/Xcode_26.4.1.app
@@ -40,11 +43,11 @@ jobs:
40
43
  run: swift package resolve
41
44
 
42
45
  - name: Build release tool
43
- # Runs `npm run build` (scripts/build.js): builds the universal
46
+ # Runs `pnpm run build` (scripts/build.js): builds the universal
44
47
  # (arm64 + x86_64) release plugin binary, copies it to
45
48
  # apple/ExpoModulesMacros, and strips it.
46
49
  working-directory: ${{ github.workspace }}
47
- run: npm run build
50
+ run: pnpm run build
48
51
 
49
52
  - name: Verify universal binary
50
53
  run: |
@@ -64,5 +67,5 @@ jobs:
64
67
  - name: Typecheck the TypeScript wrapper
65
68
  working-directory: ${{ github.workspace }}
66
69
  run: |
67
- npm install
68
- npm run typecheck
70
+ pnpm install
71
+ pnpm run typecheck
@@ -50,6 +50,9 @@ jobs:
50
50
  with:
51
51
  node-version: 24
52
52
 
53
+ - name: Setup pnpm
54
+ uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
55
+
53
56
  - name: Cache SwiftPM build
54
57
  uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
55
58
  with:
@@ -65,10 +68,10 @@ jobs:
65
68
  run: swift test
66
69
 
67
70
  - name: Build release tool
68
- # Runs `npm run build` (scripts/build.js): builds the release plugin binary for this
71
+ # Runs `pnpm run build` (scripts/build.js): builds the release plugin binary for this
69
72
  # architecture as platforms/win32-<arch>/ExpoModulesMacros.exe.
70
73
  working-directory: ${{ github.workspace }}
71
- run: npm run build
74
+ run: pnpm run build
72
75
 
73
76
  - name: Verify the release tool
74
77
  # Runs it with only the Windows directories on PATH, so it must not need the Swift runtime DLLs.
@@ -109,11 +112,11 @@ jobs:
109
112
  working-directory: ${{ github.workspace }}
110
113
  shell: pwsh
111
114
  run: |
112
- npm install
115
+ pnpm install
113
116
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
114
- npm install --no-save $env:PLATFORM_PACKAGE_TARBALL
117
+ pnpm add --save-optional $env:PLATFORM_PACKAGE_TARBALL
115
118
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
116
- npm run build:ts
119
+ pnpm run build:ts
117
120
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
118
121
  node scripts/check-scan.js .
119
122
  if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
package/AGENTS.md CHANGED
@@ -9,23 +9,27 @@
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`, 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.
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 `pnpm 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 and Linux `platforms/win32-<arch>/ExpoModulesMacros.exe` or `platforms/linux-<arch>/ExpoModulesMacros` for the host architecture (not committed; published in that package).
15
+ - `scripts/build.js` (`pnpm 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
+ - `scripts/swift-format.js` (`pnpm run swift:format`, `pnpm run swift:lint`): runs swift-format on the `.swift` files that Git tracks or doesn't ignore, with the configuration in `.swift-format`.
16
17
 
17
18
  ## Commands
18
19
 
19
- Swift 6.2 is required (Xcode 26 or newer on macOS).
20
+ Swift 6.2 is required (Xcode 26 or newer on macOS). The package manager is pnpm, at the version in `packageManager` in `package.json`.
20
21
 
21
22
  ```sh
22
23
  cd apple && swift build # debug build
23
24
  cd apple && swift test # all Swift tests
24
- npm run typecheck # TypeScript wrapper
25
- npm run build # release binary (slow; on macOS uses Rosetta for x86_64)
25
+ pnpm install # TypeScript dependencies
26
+ pnpm run typecheck # TypeScript wrapper
27
+ pnpm run build # release binary (slow; on macOS uses Rosetta for x86_64)
28
+ pnpm run swift:format # format the Swift sources in place
29
+ pnpm run swift:lint # check the formatting, as CI does
26
30
  ```
27
31
 
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.
32
+ 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 `pnpm 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. The Swift Format workflow (`.github/workflows/swift-format.yml`) builds the pinned swift-format version and runs `pnpm run swift:lint`. Code in `Sources` and `Tests` must not assume Apple platforms or POSIX paths.
29
33
 
30
34
  ## Windows
31
35
 
@@ -39,9 +43,11 @@ CI runs on macOS (`.github/workflows/swift.yml`), on Windows x64 and arm64 (`.gi
39
43
 
40
44
  ## Conventions
41
45
 
42
- - Swift uses 2-space indentation. Write new doc comments as `///` line comments, not `/** */` blocks.
46
+ - swift-format formats the Swift sources. After you change Swift code, run `pnpm run swift:format` and then `pnpm run swift:lint`, which also reports the rules that the formatter doesn't fix. `.swift-format` is a copy of the configuration in `expo/expo`, so keep the two the same. Each repository pins its own swift-format version.
47
+ - The scripts run the swift-format on `PATH`, or on macOS the one in Xcode when there is none. CI builds the version in `SWIFT_FORMAT_VERSION` (`.github/workflows/swift-format.yml`, also named in the README), which can format differently from the one in Xcode. When CI reports a finding that the local lint doesn't, fix it as CI reports it.
48
+ - Write new doc comments as `///` line comments, not `/** */` blocks.
43
49
  - Tests use Swift Testing (`import Testing`, `@Test`) with backtick-quoted descriptive names, for example ``func `Prunes .build, Pods, .git, and node_modules directories`()``.
44
- - Macro tests compare full expansions with `assertMacroExpansion` from `SwiftSyntaxMacrosGenericTestSupport`, using `indentationWidth: .spaces(2)`. Each test file wraps it in a local `assertExpansion` helper.
50
+ - Macro tests compare full expansions with `assertMacroExpansion` from `SwiftSyntaxMacrosGenericTestSupport`, using `indentationWidth: .spaces(2)`. Each test file wraps it in a local `assertExpansion` helper that passes `recordIssue` (`IssueRecording.swift`) as the failure handler.
45
51
  - Scanner tests reach internal API with `@testable import ExpoModulesScanner`. Do not make scanner types `public` only for tests.
46
52
  - `@JS` applies to functions, properties and initializers. Do not write code or comments that assume it marks only functions.
47
53
  - Code comments describe the code as it is. Do not refer to open PRs, branches or planned work.
@@ -49,7 +55,7 @@ CI runs on macOS (`.github/workflows/swift.yml`), on Windows x64 and arm64 (`.gi
49
55
  ## Changing macros
50
56
 
51
57
  - The scanner re-implements the macros' naming and member rules on the syntax tree; it does not call the macro code. When a macro changes which members it binds or how it names them (the `@JS` name override, the `on` prefix `@Event` strips, which properties `@Record` treats as fields), change `scan-exports` to match, or generated TypeScript types drift from the runtime.
52
- - The scanner only parses files that match the pre-filter built by `macroAttributeRegex` in `Core/SourceScan.swift`. A new attribute or conformance the scanner must detect has to be added there, or files that use it are skipped without an error.
58
+ - The scanner only parses files that pass the pre-filter, `MacroPrefilter` in `Core/SourceScan.swift`. Each command builds it from the `macros` and `conformances` arguments of its `scanFiles` call. A new attribute or conformance the scanner must detect has to be added to those arguments (an attribute also needs a `DetectedMacro` case), or files that use it are skipped without an error.
53
59
  - A new macro needs: the implementation in `apple/Sources/ExpoModulesMacros`, an entry in `providingMacros` in `Plugin.swift`, an expansion test file, the `#externalMacro` declaration in `expo-modules-core`, and an entry in the macro list in `README.md`.
54
60
 
55
61
  ## Contracts with other packages
@@ -63,6 +69,7 @@ CI runs on macOS (`.github/workflows/swift.yml`), on Windows x64 and arm64 (`.gi
63
69
 
64
70
  - 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`.
65
71
  - Do not edit the `version` in `package.json` by hand. The Publish workflow bumps it.
72
+ - The workflows install dependencies and run scripts with pnpm, but they pack, version and publish with npm (`npm pack`, `npm version`, `npm publish`), because the packages use npm trusted publishing (OIDC). Keep those commands on npm.
66
73
  - Commit and PR titles are short and imperative, with code identifiers in backticks, for example ``Report `@Union` types in `scan-exports` ``.
67
74
 
68
75
  ## Installed copies
package/README.md CHANGED
@@ -25,10 +25,10 @@ This package lists them as optional dependencies, and their `os`, `cpu` and (on
25
25
  # Macros
26
26
 
27
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.
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.
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. A `lazy var` is read-only from JavaScript. On the first read, its getter stores the value as a data property on the JavaScript object, so later reads don't call into native code. On a frozen object the value isn't stored, and each read calls the getter. The `.concurrent` option moves an `async` body off the JavaScript thread while still decoding and encoding on it. The `.withAsync("name")` option binds a synchronous function a second time under that name, as a function that returns a promise and runs the body on the concurrent thread pool. That function isn't isolated to the JavaScript thread, so its body must be safe to run on any thread. The macro reports a function isolated to a global actor through its own attribute or its type's. When the isolation comes from elsewhere, such as a protocol or the module's default isolation, the error points into the generated code, so mark the function `nonisolated`. 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.
29
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.
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:)`.
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.
30
+ - **`@SharedObject(_ name: String? = nil)`** on a subclass of `SharedObject`, direct or not, such as a `SharedRef<T>` subclass. Binds the class's `@JS` members through the `SharedObject` hooks that core calls when it builds the JavaScript class, and emits the JS class name, which defaults to the class name. A module exposes the class through `@ExpoModule(classes:)`, which only accepts `SharedObject` subclasses.
31
+ - **`@Record()`** on a struct, or on a final class with no inheritance clause (declare a class's protocol conformances in an extension). 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.
32
32
  - **`@Union()`** on an enum whose cases each carry one associated value. Models a TypeScript union. Synthesizes the conversions in both directions, a typed accessor per payload type, and the `JavaScriptDecodable` and `JavaScriptEncodable` conformances. Decoding is ordered: the first case whose payload decodes wins, so the more specific case goes first.
33
33
 
34
34
  How that looks in a module:
@@ -102,7 +102,7 @@ The binary is a compiled executable, so each call still spawns a process. What t
102
102
 
103
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.
104
104
 
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.
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 `pnpm run build` output in `platforms/<platform>-<arch>` when the platform package isn't installed, for example in this repository.
106
106
 
107
107
  The module and type names in `#externalMacro` must stay in sync with `apple/Sources/ExpoModulesMacros/Plugin.swift`.
108
108
 
@@ -116,7 +116,22 @@ swift build
116
116
  swift test
117
117
  ```
118
118
 
119
- `npm run build` runs `scripts/build.js`, which builds the release binary with SwiftPM's native build system (Swift Build, the default since Swift 6.4, doesn't build a macro tool that no target in the package uses).
119
+ The scripts and the TypeScript wrapper use [pnpm](https://pnpm.io), at the version in `packageManager` in `package.json`. Run `pnpm install` to install the TypeScript dependencies, and `pnpm run typecheck` to check the wrapper.
120
+
121
+ The Swift sources are formatted with [swift-format](https://github.com/swiftlang/swift-format), using the same configuration (`.swift-format`) as the `expo/expo` repository. `pnpm run swift:format` formats the `.swift` files that Git tracks or doesn't ignore in place, and `pnpm run swift:lint` checks them without changes, as CI does.
122
+
123
+ The scripts run the `swift-format` on `PATH`, or on macOS the one in the selected Xcode when there is none on `PATH`. CI uses swift-format `604.0.0`, which can format differently from the one in Xcode. To get the version that CI uses, build it from source:
124
+
125
+ ```sh
126
+ git clone --depth 1 --branch 604.0.0 https://github.com/swiftlang/swift-format.git /tmp/swift-format
127
+ (cd /tmp/swift-format && swift build -c release --product swift-format)
128
+ mkdir -p ~/.local/bin
129
+ cp "$(cd /tmp/swift-format && swift build -c release --show-bin-path)/swift-format" ~/.local/bin/
130
+ ```
131
+
132
+ Then add `~/.local/bin` to `PATH`.
133
+
134
+ `pnpm run build` runs `scripts/build.js`, which builds the release binary with SwiftPM's native build system (Swift Build, the default since Swift 6.4, doesn't build a macro tool that no target in the package uses).
120
135
 
121
136
  - 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.
122
137
  - 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.
Binary file
@@ -1,13 +1,13 @@
1
1
  {
2
- "originHash" : "f142f4d4b3ea8f139f9a2f206e382cf98031da4c8ea8691ddec05cb0c9d2285f",
2
+ "originHash" : "a90a0c6d6b4023098b6a673ded4f314ec06c219ded1559e5d5595f23c7dc323b",
3
3
  "pins" : [
4
4
  {
5
5
  "identity" : "swift-syntax",
6
6
  "kind" : "remoteSourceControl",
7
7
  "location" : "https://github.com/swiftlang/swift-syntax.git",
8
8
  "state" : {
9
- "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1",
10
- "version" : "603.0.2"
9
+ "revision" : "050f1a346fbbac0ca2cfb15a95274f7bd1cf0ccf",
10
+ "version" : "604.0.0"
11
11
  }
12
12
  }
13
13
  ],
@@ -9,7 +9,7 @@ let package = Package(
9
9
  name: "ExpoModulesMacros",
10
10
  platforms: [.macOS(.v13)],
11
11
  dependencies: [
12
- .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "603.0.0-latest")
12
+ .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "604.0.0-latest")
13
13
  ],
14
14
  targets: [
15
15
  // The plugin executable doubles as the scanner CLI: the compiler launches it without arguments
@@ -44,7 +44,6 @@ if FileManager.default.fileExists(atPath: Context.packageDirectory + "/Tests") {
44
44
  name: "ExpoModulesMacrosTests",
45
45
  dependencies: [
46
46
  "ExpoModulesMacros",
47
- .product(name: "SwiftSyntaxMacrosTestSupport", package: "swift-syntax"),
48
47
  .product(name: "SwiftSyntaxMacrosGenericTestSupport", package: "swift-syntax"),
49
48
  ]
50
49
  )
@@ -189,7 +189,9 @@ internal func diagnoseClosureTypes(
189
189
  message: ClosureDiagnosticMessage(
190
190
  "A @JS function can't return a closure: a native closure can't be encoded to a JavaScript function.",
191
191
  id: "js-closure-return"
192
- )))
192
+ )
193
+ )
194
+ )
193
195
  }
194
196
  return
195
197
  }
@@ -199,14 +201,17 @@ internal func diagnoseClosureTypes(
199
201
  }
200
202
  if let varDecl = declaration.as(VariableDeclSyntax.self),
201
203
  let type = varDecl.bindings.first?.typeAnnotation?.type,
202
- let nested = firstFunctionType(in: type) {
204
+ let nested = firstFunctionType(in: type)
205
+ {
203
206
  context.diagnose(
204
207
  Diagnostic(
205
208
  node: nested,
206
209
  message: ClosureDiagnosticMessage(
207
210
  "A @JS property can't have a closure type: a native closure can't be encoded to a JavaScript function.",
208
211
  id: "js-closure-property"
209
- )))
212
+ )
213
+ )
214
+ )
210
215
  }
211
216
  }
212
217
 
@@ -223,7 +228,9 @@ private func diagnoseClosureParameters(
223
228
  message: ClosureDiagnosticMessage(
224
229
  "A closure in a @JS argument must be the argument's own type, optionally wrapped in an optional. Closures inside other types, such as arrays or dictionaries, aren't supported.",
225
230
  id: "js-closure-nested-in-type"
226
- )))
231
+ )
232
+ )
233
+ )
227
234
  }
228
235
  continue
229
236
  }
@@ -245,7 +252,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
245
252
  message: ClosureDiagnosticMessage(
246
253
  "'@\(name)' isn't supported on a @JS closure argument. The closure calls JavaScript through its own thread hop, so only '@escaping' and '@Sendable' are allowed.",
247
254
  id: "js-closure-attribute"
248
- )))
255
+ )
256
+ )
257
+ )
249
258
  }
250
259
 
251
260
  if let throwsClause = functionType.effectSpecifiers?.throwsClause, throwsClause.type != nil {
@@ -255,7 +264,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
255
264
  message: ClosureDiagnosticMessage(
256
265
  "A @JS closure argument can't use typed throws: the error from JavaScript can be any error. Write 'throws' without a type.",
257
266
  id: "js-closure-typed-throws"
258
- )))
267
+ )
268
+ )
269
+ )
259
270
  }
260
271
 
261
272
  for parameter in closure.parameters {
@@ -266,7 +277,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
266
277
  message: ClosureDiagnosticMessage(
267
278
  "A @JS closure argument can't take an '\(specifier)' parameter: its value is encoded and sent to JavaScript, which can't write it back or share its isolation.",
268
279
  id: "js-closure-parameter-specifier"
269
- )))
280
+ )
281
+ )
282
+ )
270
283
  }
271
284
  if parameter.isVariadic {
272
285
  context.diagnose(
@@ -275,7 +288,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
275
288
  message: ClosureDiagnosticMessage(
276
289
  "A @JS closure argument can't take a variadic parameter. Use an array instead.",
277
290
  id: "js-closure-variadic-parameter"
278
- )))
291
+ )
292
+ )
293
+ )
279
294
  }
280
295
  }
281
296
 
@@ -290,7 +305,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
290
305
  message: ClosureDiagnosticMessage(
291
306
  "A @JS closure argument can't take or return another closure.",
292
307
  id: "js-closure-nested"
293
- )))
308
+ )
309
+ )
310
+ )
294
311
  } else if mentionsFreeFormAny(type) {
295
312
  context.diagnose(
296
313
  Diagnostic(
@@ -298,7 +315,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
298
315
  message: ClosureDiagnosticMessage(
299
316
  "A @JS closure argument can't use the free-form '\(type.trimmedDescription)'. Use a concrete type, or 'JavaScriptValue' to pass a JS value through unchanged.",
300
317
  id: "js-closure-free-form"
301
- )))
318
+ )
319
+ )
320
+ )
302
321
  }
303
322
  }
304
323
 
@@ -310,7 +329,9 @@ private func diagnoseClosure(_ closure: JSClosureType, in context: some MacroExp
310
329
  "A @JS closure argument that returns a value must be 'throws': when the JavaScript function throws, the closure has no value to return.",
311
330
  id: "js-closure-requires-throws"
312
331
  ),
313
- fixIts: [insertThrowsFixIt(for: functionType)]))
332
+ fixIts: [insertThrowsFixIt(for: functionType)]
333
+ )
334
+ )
314
335
  }
315
336
  }
316
337
 
@@ -321,13 +342,16 @@ private func insertThrowsFixIt(for functionType: FunctionTypeSyntax) -> FixIt {
321
342
  var newType = functionType
322
343
  if var effectSpecifiers = functionType.effectSpecifiers, let asyncSpecifier = effectSpecifiers.asyncSpecifier {
323
344
  effectSpecifiers.throwsClause = ThrowsClauseSyntax(
324
- throwsSpecifier: .keyword(.throws, trailingTrivia: asyncSpecifier.trailingTrivia))
345
+ throwsSpecifier: .keyword(.throws, trailingTrivia: asyncSpecifier.trailingTrivia)
346
+ )
325
347
  effectSpecifiers.asyncSpecifier = asyncSpecifier.with(\.trailingTrivia, .space)
326
348
  newType.effectSpecifiers = effectSpecifiers
327
349
  } else {
328
350
  newType.effectSpecifiers = TypeEffectSpecifiersSyntax(
329
351
  throwsClause: ThrowsClauseSyntax(
330
- throwsSpecifier: .keyword(.throws, trailingTrivia: functionType.rightParen.trailingTrivia)))
352
+ throwsSpecifier: .keyword(.throws, trailingTrivia: functionType.rightParen.trailingTrivia)
353
+ )
354
+ )
331
355
  newType.rightParen = functionType.rightParen.with(\.trailingTrivia, .space)
332
356
  }
333
357
  return FixIt(
@@ -360,7 +384,8 @@ internal func firstFunctionType(in type: some SyntaxProtocol) -> FunctionTypeSyn
360
384
  /// same type as `(Int) -> Void`.
361
385
  private func unparenthesized(_ type: TypeSyntax) -> TypeSyntax {
362
386
  if let tuple = type.as(TupleTypeSyntax.self), tuple.elements.count == 1,
363
- let element = tuple.elements.first, element.firstName == nil {
387
+ let element = tuple.elements.first, element.firstName == nil
388
+ {
364
389
  return unparenthesized(element.type)
365
390
  }
366
391
  return type