expo-modules-macros 0.16.0 → 0.17.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 (29) hide show
  1. package/.github/workflows/publish.yml +93 -7
  2. package/.github/workflows/swift.yml +2 -2
  3. package/.github/workflows/windows.yml +144 -0
  4. package/AGENTS.md +13 -7
  5. package/README.md +15 -3
  6. package/apple/ExpoModulesMacros +0 -0
  7. package/apple/Package.resolved +3 -3
  8. package/apple/Package.swift +1 -1
  9. package/apple/Sources/ExpoModulesMacros/DecorateModuleBuilder.swift +7 -7
  10. package/apple/Sources/ExpoModulesMacros/ExpoModulesOptimizedMacro.swift +0 -1
  11. package/apple/Sources/ExpoModulesMacros/FreeFormTypes.swift +10 -7
  12. package/apple/Sources/ExpoModulesMacros/JSConstructor.swift +1 -1
  13. package/apple/Sources/ExpoModulesMacros/JSMacro.swift +4 -5
  14. package/apple/Sources/ExpoModulesMacros/Plugin.swift +8 -1
  15. package/apple/Sources/ExpoModulesMacros/RecordMacro.swift +1 -1
  16. package/apple/Sources/ExpoModulesMacros/ViewPropsMacro.swift +14 -3
  17. package/apple/Sources/ExpoModulesScanner/CLI.swift +8 -13
  18. package/apple/Sources/ExpoModulesScanner/Core/Detection.swift +0 -2
  19. package/apple/Sources/ExpoModulesScanner/Core/ScanBuildConfiguration.swift +5 -0
  20. package/apple/Sources/ExpoModulesScanner/Core/SourceScan.swift +110 -29
  21. package/apple/Sources/ExpoModulesScanner/Core/StandardStreams.swift +45 -0
  22. package/apple/Sources/ExpoModulesScanner/Exports/ExportedSurface.swift +0 -2
  23. package/apple/Sources/ExpoModulesScanner/Exports/ResolveRefs.swift +0 -2
  24. package/apple/Sources/ExpoModulesScanner/Exports/ScanExports.swift +6 -3
  25. package/apple/Sources/ExpoModulesScanner/Modules/ScanModules.swift +6 -3
  26. package/build/index.d.ts +11 -2
  27. package/build/index.js +40 -4
  28. package/package.json +6 -2
  29. package/apple/build.js +0 -138
@@ -16,15 +16,65 @@ on:
16
16
  - preminor
17
17
  - premajor
18
18
  - prerelease
19
+ dry_run:
20
+ description: Dry run (build everything and run `npm publish --dry-run`, but don't publish, commit, tag or create a release)
21
+ type: boolean
22
+ default: false
19
23
 
20
24
  concurrency:
21
25
  group: publish-${{ github.workflow }}
22
26
  cancel-in-progress: false
23
27
 
24
28
  jobs:
29
+ build-windows:
30
+ name: Build Windows tool (${{ matrix.arch }})
31
+ # SwiftPM builds macro tools only for the host, so each architecture builds on a runner of its own.
32
+ runs-on: ${{ matrix.runner }}
33
+ timeout-minutes: 30
34
+ strategy:
35
+ matrix:
36
+ include:
37
+ - arch: x64
38
+ runner: windows-latest
39
+ build_arch: amd64
40
+ - arch: arm64
41
+ runner: windows-11-arm
42
+ build_arch: arm64
43
+ steps:
44
+ - name: Checkout
45
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
46
+
47
+ - name: Install Swift
48
+ uses: compnerd/gha-setup-swift@bbae8ce86bac5a3449f3992b3d88bc4ca25f2057 # v0.5.0
49
+ with:
50
+ swift-version: swift-6.4.0-release
51
+ swift-build: 6.4.0-RELEASE
52
+ build_arch: ${{ matrix.build_arch }}
53
+
54
+ - name: Setup Node
55
+ uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
56
+ with:
57
+ node-version: 24
58
+
59
+ - name: Build release tool
60
+ # Runs scripts/build.js: builds the release plugin binary for this architecture as
61
+ # platforms/win32-<arch>/ExpoModulesMacros.exe.
62
+ run: npm run build
63
+
64
+ - name: Upload tool
65
+ uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
66
+ with:
67
+ # Named after the package directory: the publish job downloads every artifact into
68
+ # `platforms/`, and each one lands in a directory of its name.
69
+ name: win32-${{ matrix.arch }}
70
+ path: platforms/win32-${{ matrix.arch }}/ExpoModulesMacros.exe
71
+ if-no-files-found: error
72
+
25
73
  publish:
26
74
  name: Build and publish to npm
27
- # Builds the native macOS tool binary, so it must run on macOS.
75
+ needs: build-windows
76
+ # Builds the native macOS tool binary, so it must run on macOS. The Windows binaries come from the
77
+ # build-windows jobs.
28
78
  # The package declares swift-tools-version 6.2, which requires Xcode 26+.
29
79
  runs-on: macos-26
30
80
  permissions:
@@ -42,9 +92,9 @@ jobs:
42
92
  token: ${{ secrets.GITHUB_TOKEN }}
43
93
 
44
94
  - name: Setup Node
45
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
95
+ uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
46
96
  with:
47
- node-version: 22
97
+ node-version: 24
48
98
  registry-url: https://registry.npmjs.org
49
99
 
50
100
  - name: Update npm
@@ -64,13 +114,24 @@ jobs:
64
114
  version=$(npm version "${{ inputs.release_type }}" --no-git-tag-version)
65
115
  echo "version=$version" >> "$GITHUB_OUTPUT"
66
116
  echo "Bumping to $version"
117
+ # npm 11 doesn't publish a prerelease version (one with a hyphen) without a dist-tag, and
118
+ # it must not become `latest` anyway.
119
+ case "$version" in
120
+ *-*) tag=next ;;
121
+ *) tag=latest ;;
122
+ esac
123
+ echo "tag=$tag" >> "$GITHUB_OUTPUT"
124
+ echo "Publishing with the dist-tag $tag"
125
+ # The platform packages get the same version, and `expo-modules-macros` lists them as
126
+ # optional dependencies with that exact version.
127
+ node scripts/set-platform-versions.js
67
128
 
68
129
  - name: Select Xcode
69
130
  # Pin to an Xcode that ships Swift 6.2 (required by Package.swift).
70
131
  run: sudo xcode-select -switch /Applications/Xcode_26.4.1.app
71
132
 
72
133
  - name: Cache SwiftPM build
73
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
134
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
74
135
  with:
75
136
  path: apple/.build
76
137
  key: spm-${{ github.repository }}-${{ runner.os }}-${{ hashFiles('apple/Package.resolved') }}
@@ -86,18 +147,42 @@ jobs:
86
147
  run: npm install
87
148
 
88
149
  - name: Build release tool
89
- # Runs apple/build.js: builds the release plugin binary,
150
+ # Runs scripts/build.js: builds the release plugin binary,
90
151
  # copies it to apple/ExpoModulesMacros, and strips it.
91
152
  run: npm run build
92
153
 
154
+ - name: Download Windows tools
155
+ # Each artifact goes into the directory of its name: `platforms/win32-<arch>`.
156
+ uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
157
+ with:
158
+ pattern: win32-*
159
+ path: platforms
160
+
161
+ - name: Verify Windows tools
162
+ run: ls -l platforms/win32-x64/ExpoModulesMacros.exe platforms/win32-arm64/ExpoModulesMacros.exe
163
+
164
+ - name: Publish the Windows packages
165
+ # Published before `expo-modules-macros`, whose optional dependencies point at them, so that
166
+ # package is never on npm without them. Each package needs trusted publishing (OIDC)
167
+ # configured on npmjs.com for this workflow.
168
+ env:
169
+ DRY_RUN: ${{ inputs.dry_run && '--dry-run' || '' }}
170
+ run: |
171
+ for arch in x64 arm64; do
172
+ npm publish --access public --tag "${{ steps.bump.outputs.tag }}" $DRY_RUN "./platforms/win32-$arch"
173
+ done
174
+
93
175
  - name: Publish
94
176
  # Authentication is handled via OIDC trusted publishing — no token needed.
95
177
  # npm generates provenance automatically when publishing via OIDC.
96
- run: npm publish --access public
178
+ env:
179
+ DRY_RUN: ${{ inputs.dry_run && '--dry-run' || '' }}
180
+ run: npm publish --access public --tag "${{ steps.bump.outputs.tag }}" $DRY_RUN
97
181
 
98
- # Everything below only runs once the publish above has succeeded.
182
+ # Everything below only runs once the publish above has succeeded, and not in a dry run.
99
183
 
100
184
  - name: Commit, tag and push
185
+ if: ${{ !inputs.dry_run }}
101
186
  run: |
102
187
  version="${{ steps.bump.outputs.version }}"
103
188
  git commit -am "Release $version"
@@ -107,6 +192,7 @@ jobs:
107
192
  git push --follow-tags origin HEAD
108
193
 
109
194
  - name: Create GitHub release
195
+ if: ${{ !inputs.dry_run }}
110
196
  env:
111
197
  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
112
198
  # Tag prerelease versions (those containing a hyphen) as prereleases.
@@ -29,7 +29,7 @@ jobs:
29
29
  run: sudo xcode-select -switch /Applications/Xcode_26.4.1.app
30
30
 
31
31
  - name: Cache SwiftPM build
32
- uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
32
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
33
33
  with:
34
34
  path: apple/.build
35
35
  key: spm-${{ github.repository }}-${{ runner.os }}-${{ hashFiles('apple/Package.resolved') }}
@@ -40,7 +40,7 @@ jobs:
40
40
  run: swift package resolve
41
41
 
42
42
  - name: Build release tool
43
- # Runs `npm run build` (apple/build.js): builds the universal
43
+ # Runs `npm run build` (scripts/build.js): builds the universal
44
44
  # (arm64 + x86_64) release plugin binary, copies it to
45
45
  # apple/ExpoModulesMacros, and strips it.
46
46
  working-directory: ${{ github.workspace }}
@@ -0,0 +1,144 @@
1
+ name: Windows
2
+
3
+ # Builds and tests the package on Windows, and checks the release tool there. The Windows binaries
4
+ # aren't published yet, so this runs apart from the macOS workflow.
5
+
6
+ on:
7
+ push:
8
+ branches: [main]
9
+ pull_request:
10
+ branches: [main]
11
+ workflow_dispatch: {}
12
+
13
+ concurrency:
14
+ group: ci-${{ github.workflow }}-${{ github.ref }}
15
+ cancel-in-progress: true
16
+
17
+ defaults:
18
+ run:
19
+ working-directory: apple
20
+
21
+ jobs:
22
+ windows:
23
+ name: Build & test (Windows ${{ matrix.arch }})
24
+ runs-on: ${{ matrix.runner }}
25
+ # A run takes about 4 to 5 minutes. The limit stops a hang, for example in the plugin server,
26
+ # from holding a runner for the default 6 hours.
27
+ timeout-minutes: 30
28
+ strategy:
29
+ fail-fast: false
30
+ matrix:
31
+ include:
32
+ - arch: x64
33
+ runner: windows-latest
34
+ build_arch: amd64
35
+ - arch: arm64
36
+ runner: windows-11-arm
37
+ build_arch: arm64
38
+ steps:
39
+ - name: Checkout
40
+ uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
41
+
42
+ - name: Install Swift
43
+ uses: compnerd/gha-setup-swift@bbae8ce86bac5a3449f3992b3d88bc4ca25f2057 # v0.5.0
44
+ with:
45
+ swift-version: swift-6.4.0-release
46
+ swift-build: 6.4.0-RELEASE
47
+ build_arch: ${{ matrix.build_arch }}
48
+
49
+ - name: Setup Node
50
+ uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
51
+ with:
52
+ node-version: 24
53
+
54
+ - name: Cache SwiftPM build
55
+ uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
56
+ with:
57
+ path: apple/.build
58
+ key: spm-${{ github.repository }}-${{ runner.os }}-${{ matrix.arch }}-${{ hashFiles('apple/Package.resolved') }}
59
+ restore-keys: |
60
+ spm-${{ github.repository }}-${{ runner.os }}-${{ matrix.arch }}-
61
+
62
+ - name: Build
63
+ run: swift build
64
+
65
+ - name: Test
66
+ run: swift test
67
+
68
+ - name: Build release tool
69
+ # Runs `npm run build` (scripts/build.js): builds the release plugin binary for this
70
+ # architecture as platforms/win32-<arch>/ExpoModulesMacros.exe.
71
+ working-directory: ${{ github.workspace }}
72
+ run: npm run build
73
+
74
+ - name: Verify the release tool
75
+ # Runs it with only the Windows directories on PATH, so it must not need the Swift runtime DLLs.
76
+ # With no arguments it is the plugin server, which exits when stdin closes; with arguments it
77
+ # is the scanner CLI.
78
+ shell: pwsh
79
+ run: |
80
+ $exe = (Resolve-Path "../platforms/win32-${{ matrix.arch }}/ExpoModulesMacros.exe").Path
81
+ $env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
82
+ cmd /c "`"$exe`" < NUL"
83
+ if ($LASTEXITCODE -ne 0) { throw "The plugin server exited with $LASTEXITCODE" }
84
+ & $exe --help | Out-Null
85
+ if ($LASTEXITCODE -ne 0) { throw "The scanner CLI exited with $LASTEXITCODE" }
86
+
87
+ - name: Pack the platform package
88
+ # Checks what `npm pack` would publish from `platforms/win32-<arch>`, where the release build
89
+ # put the tool: the executable, the README, the license and the manifest, with the `os` and
90
+ # `cpu` fields for this machine.
91
+ working-directory: ${{ github.workspace }}
92
+ shell: pwsh
93
+ run: |
94
+ # The `./` matters: npm reads `platforms/win32-<arch>` as a GitHub repository.
95
+ $package = "./platforms/win32-${{ matrix.arch }}"
96
+ $pack = npm pack $package --pack-destination $env:RUNNER_TEMP --json | ConvertFrom-Json
97
+ if ($LASTEXITCODE -ne 0) { throw "npm pack failed" }
98
+ $files = ($pack[0].files | ForEach-Object { $_.path } | Sort-Object) -join ","
99
+ Write-Host "$($pack[0].filename): $files ($($pack[0].size) bytes packed)"
100
+ if ($files -ne "ExpoModulesMacros.exe,LICENSE,package.json,README.md") { throw "Unexpected files: $files" }
101
+ $manifest = Get-Content "$package/package.json" -Raw | ConvertFrom-Json
102
+ if (($manifest.os -join ",") -ne "win32" -or ($manifest.cpu -join ",") -ne "${{ matrix.arch }}") {
103
+ throw "Unexpected os/cpu: $($manifest.os) / $($manifest.cpu)"
104
+ }
105
+ "PLATFORM_PACKAGE_TARBALL=$(Join-Path $env:RUNNER_TEMP $pack[0].filename)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
106
+
107
+ - name: Scan through the TypeScript wrapper
108
+ # Installs the packed platform package. The wrapper must find the executable in
109
+ # `@expo/modules-macros-win32-<arch>`, as it would in a project, not the local build.
110
+ working-directory: ${{ github.workspace }}
111
+ shell: pwsh
112
+ run: |
113
+ npm install
114
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
115
+ npm install --no-save $env:PLATFORM_PACKAGE_TARBALL
116
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
117
+ npm run build:ts
118
+ if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
119
+ $fixture = Join-Path $env:RUNNER_TEMP "scan-fixture"
120
+ New-Item -ItemType Directory -Force $fixture | Out-Null
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
package/AGENTS.md CHANGED
@@ -9,22 +9,28 @@
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 Windows and isn't committed.
12
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.
13
14
  - `src/`: the TypeScript wrapper around the scanner CLI. `types.ts` mirrors the Swift `Codable` output types by hand.
14
- - `apple/build.js`: builds the universal release binary that is committed under `apple/`.
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
16
 
16
17
  ## Commands
17
18
 
18
- Swift 6.2 is required (Xcode 26 or newer).
19
+ Swift 6.2 is required (Xcode 26 or newer on macOS).
19
20
 
20
21
  ```sh
21
22
  cd apple && swift build # debug build
22
23
  cd apple && swift test # all Swift tests
23
24
  npm run typecheck # TypeScript wrapper
24
- npm run build # universal release binary (slow; uses Rosetta for x86_64)
25
+ npm run build # release binary (slow; on macOS uses Rosetta for x86_64)
25
26
  ```
26
27
 
27
- CI (`.github/workflows/swift.yml`) runs the release build, checks both binary slices, then runs `swift test` and `npm run typecheck`.
28
+ CI runs on macOS (`.github/workflows/swift.yml`) and on Windows x64 and arm64 (`.github/workflows/windows.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, which must find the executable there. Code in `Sources` and `Tests` must not assume Apple platforms or POSIX paths.
29
+
30
+ ## Windows
31
+
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
+ - 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.
28
34
 
29
35
  ## Conventions
30
36
 
@@ -46,14 +52,14 @@ CI (`.github/workflows/swift.yml`) runs the release build, checks both binary sl
46
52
  - **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`.
47
53
  - **Generated code calls `expo-modules-core` API.** An expansion that uses a new core symbol only works with a core version that has it.
48
54
  - **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.
49
- - **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. Renaming or moving the binary needs a matching change in `expo/expo`.
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`.
50
56
 
51
57
  ## Releases
52
58
 
53
- - 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.
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.
54
60
  - Do not edit the `version` in `package.json` by hand. The Publish workflow bumps it.
55
61
  - Commit and PR titles are short and imperative, with code identifiers in backticks, for example ``Report `@Union` types in `scan-exports` ``.
56
62
 
57
63
  ## Installed copies
58
64
 
59
- In `node_modules`, the compiler runs the prebuilt binary `apple/ExpoModulesMacros`; 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.
65
+ In `node_modules`, the compiler runs the prebuilt binary `apple/ExpoModulesMacros` (on Windows, `ExpoModulesMacros.exe` from `@expo/modules-macros-win32-<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,6 +15,8 @@ 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, `@expo/modules-macros-win32-x64` and `@expo/modules-macros-win32-arm64`, each with an `ExpoModulesMacros.exe`. This package lists them as optional dependencies, and their `os` and `cpu` fields make npm, pnpm and Yarn install only the one that matches a Windows machine, so other machines don't download them. The Swift runtime is linked in statically, so the executables run without a Swift toolchain; they need only Windows system libraries and the Microsoft Visual C++ runtime.
19
+
18
20
  # Macros
19
21
 
20
22
  - **`@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.
@@ -67,7 +69,6 @@ subcommands:
67
69
  scan-exports deep scan of the full JS-exported surface (type generation)
68
70
 
69
71
  options (scan-modules only):
70
- --platform <os> evaluate '#if os(...)' against this platform
71
72
  --define <flag> treat a conditional compilation flag as set; repeatable
72
73
  ```
73
74
 
@@ -96,11 +97,13 @@ The binary is a compiled executable, so each call still spawns a process. What t
96
97
 
97
98
  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.
98
99
 
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/win32-<arch>` when the platform package isn't installed, for example in this repository.
101
+
99
102
  The module and type names in `#externalMacro` must stay in sync with `apple/Sources/ExpoModulesMacros/Plugin.swift`.
100
103
 
101
104
  # Development
102
105
 
103
- Requires macOS 13 or newer and a toolchain with Swift 6.2, which means Xcode 26 or newer.
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).
104
107
 
105
108
  ```sh
106
109
  cd apple
@@ -108,12 +111,21 @@ swift build
108
111
  swift test
109
112
  ```
110
113
 
111
- `npm run build` runs `apple/build.js`, which builds the release binary 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. The resulting binary is committed to the repository.
114
+ `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).
115
+
116
+ - 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
+ - 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.
118
+
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/`.
112
120
 
113
121
  # Releasing
114
122
 
115
123
  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.
116
124
 
125
+ Windows jobs (x64 and arm64) build the `.exe` files first. 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 two Windows 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
+
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 three packages, but doesn't publish, commit, tag or create a release. Use it to check a change to the workflow.
128
+
117
129
  # Contributing
118
130
 
119
131
  Contributions are very welcome! Please refer to the guidelines described in the [contributing guide](https://github.com/expo/expo#contributing).
Binary file
@@ -1,13 +1,13 @@
1
1
  {
2
- "originHash" : "4832fa479257148c1acb46952c02540c156db6e744797638db69be31154d5ad2",
2
+ "originHash" : "f142f4d4b3ea8f139f9a2f206e382cf98031da4c8ea8691ddec05cb0c9d2285f",
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" : "4799286537280063c85a32f09884cfbca301b1a1",
10
- "version" : "602.0.0"
9
+ "revision" : "79e4b74a295b6eb74a8b585e3a39d29e70c1dbd1",
10
+ "version" : "603.0.2"
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: "602.0.0-latest")
12
+ .package(url: "https://github.com/swiftlang/swift-syntax.git", from: "603.0.0-latest")
13
13
  ],
14
14
  targets: [
15
15
  // The plugin executable doubles as the scanner CLI: the compiler launches it without arguments
@@ -154,7 +154,7 @@ internal struct JSFunction {
154
154
  lines.append(contentsOf: encodeResultLines().map { " " + $0 })
155
155
  } else {
156
156
  lines.append(" \(callExpression(receiver: receiver, arity: arity))")
157
- lines.append(" return .undefined")
157
+ lines.append(" return .undefined()")
158
158
  }
159
159
  lines.append("}")
160
160
  return lines
@@ -204,7 +204,7 @@ internal struct JSFunction {
204
204
 
205
205
  /// The flat (single-arity) call-and-encode lines used when no trailing parameter is omittable:
206
206
  /// `let result = <callee>.f(...)` then the return encode (or the no-return `<callee>.f(...)` +
207
- /// `.undefined`).
207
+ /// `.undefined()`).
208
208
  private func callAndEncodeLines(receiver: Receiver, arity: Int, decodingFrom: Int) -> [String] {
209
209
  var lines: [String] = []
210
210
  for index in decodingFrom..<arity {
@@ -215,7 +215,7 @@ internal struct JSFunction {
215
215
  lines.append(contentsOf: encodeResultLines())
216
216
  } else {
217
217
  lines.append(callExpression(receiver: receiver, arity: arity))
218
- lines.append("return .undefined")
218
+ lines.append("return .undefined()")
219
219
  }
220
220
  return lines
221
221
  }
@@ -225,7 +225,7 @@ internal struct JSFunction {
225
225
  /// value as the primitive's `toJavaScriptValue(in:)` except for `Int`/`UInt`, where it range-checks
226
226
  /// and throws instead of silently encoding an out-of-safe-range value as a lossy number — the
227
227
  /// catchable error is the right behavior, and matches how non-primitive integers already encode. A
228
- /// no-return function returns `.undefined` instead.
228
+ /// no-return function returns `.undefined()` instead.
229
229
  ///
230
230
  /// For an `async` function the encode must run on the JS thread. An `async` body may suspend and
231
231
  /// resume on an arbitrary cooperative-pool thread, and encoding a heap-allocated JS value (a string,
@@ -235,7 +235,7 @@ internal struct JSFunction {
235
235
  /// which always run on the JS thread — keep encoding directly without the wrapper.
236
236
  private func encodeResultLines() -> [String] {
237
237
  guard let returnType else {
238
- return ["return .undefined"]
238
+ return ["return .undefined()"]
239
239
  }
240
240
  let encode = "try \(expressionType(returnType)).encode(result, in: runtime)"
241
241
  if isAsync {
@@ -340,7 +340,7 @@ internal struct JSProperty {
340
340
  let setDecode = "\(callee).\(swiftName) = try \(decodeCall(valueType, from: "arguments.unownedValue(at: 0)"))"
341
341
  lines.append(
342
342
  accessorClosure(
343
- descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined"))
343
+ descriptorName, "set", receiver: receiver, body: "\(unwrap)\(setDecode)\nreturn .undefined()"))
344
344
  }
345
345
 
346
346
  lines.append("\(object).defineProperty(\"\(jsName)\", descriptor: \(descriptorName))")
@@ -437,7 +437,7 @@ internal func buildDecorateSharedObjectPhase(
437
437
  }
438
438
 
439
439
  /// True when a return clause is absent or written as `Void` / `()` — i.e. the function returns
440
- /// nothing JS-visible, so the binding returns `.undefined`.
440
+ /// nothing JS-visible, so the binding returns `.undefined()`.
441
441
  private func isVoidType(_ type: TypeSyntax?) -> Bool {
442
442
  guard let type else {
443
443
  return true
@@ -1,4 +1,3 @@
1
- import Foundation
2
1
  import SwiftSyntax
3
2
  import SwiftSyntaxBuilder
4
3
  import SwiftSyntaxMacros
@@ -30,13 +30,16 @@ private let freeFormDecodeMethods: [String: String] = [
30
30
  "[String:Any]": "decodeAnyDictionary",
31
31
  ]
32
32
 
33
- /// The type-safe alternative a free-form type's diagnostic steers to: the same shape with the `Any`
34
- /// element replaced by `JavaScriptValue`, which conforms to the codable protocols. Keyed by the
35
- /// free-form type's normalized spelling; spelled with conventional spacing for the message.
33
+ /// The type-safe alternative a free-form type's diagnostic steers to: the JS wrapper of the same shape
34
+ /// (`JavaScriptArray` for an array, `JavaScriptObject` for a dictionary, `JavaScriptValue` for an
35
+ /// untyped value). The wrappers conform to the codable protocols and keep the value unconverted; a
36
+ /// non-copyable `JavaScriptValue` cannot be an element of a Swift array or dictionary, so the container
37
+ /// shapes map to a wrapper rather than to a container of values. Keyed by the free-form type's
38
+ /// normalized spelling.
36
39
  private let typedFreeFormReplacements: [String: String] = [
37
40
  "Any": "JavaScriptValue",
38
- "[Any]": "[JavaScriptValue]",
39
- "[String:Any]": "[String: JavaScriptValue]",
41
+ "[Any]": "JavaScriptArray",
42
+ "[String:Any]": "JavaScriptObject",
40
43
  ]
41
44
 
42
45
  /// True when a boundary type (as written) is one of the free-form spellings, ignoring internal
@@ -54,8 +57,8 @@ internal func freeFormDecodeMethod(for type: String) -> String? {
54
57
  return freeFormDecodeMethods[normalizedTypeSpelling(type)]
55
58
  }
56
59
 
57
- /// The type-safe alternative to suggest in place of a free-form type (its `Any` element replaced by
58
- /// `JavaScriptValue`), or `nil` when the type isn't free-form.
60
+ /// The type-safe alternative to suggest in place of a free-form type (the JS wrapper of the same shape),
61
+ /// or `nil` when the type isn't free-form.
59
62
  internal func typedFreeFormReplacement(for type: String) -> String? {
60
63
  return typedFreeFormReplacements[normalizedTypeSpelling(type)]
61
64
  }
@@ -51,7 +51,7 @@ internal struct JSConstructor {
51
51
  func buildConstructor(typeName: String) -> DeclSyntax {
52
52
  return """
53
53
  @JavaScriptActor
54
- public override class func _constructSharedObject(this: JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
54
+ public override class func _constructSharedObject(this: borrowing JavaScriptValue, arguments: borrowing JavaScriptValuesBuffer, in runtime: JavaScriptRuntime) throws -> SharedObject? {
55
55
  \(raw: bodyStatements(typeName: typeName, indent: " "))
56
56
  }
57
57
  """
@@ -120,7 +120,7 @@ private func insertAsyncFixIt(for funcDecl: FunctionDeclSyntax) -> FixIt {
120
120
  /// `JavaScriptValue.decodeAny…`; there is no free-form encode, so any position that encodes is a hard
121
121
  /// error. That means:
122
122
  /// - a function/constructor **parameter** typed free-form gets a warning steering to
123
- /// `[String: JavaScriptValue]` (the type-safe alternative), but compiles;
123
+ /// `JavaScriptValue` (the type-safe alternative), but compiles;
124
124
  /// - a function **return** typed free-form is an error (it would need to encode);
125
125
  /// - a **property** typed free-form is an error regardless of settability, because its getter always
126
126
  /// encodes.
@@ -156,9 +156,8 @@ private func diagnoseFreeFormTypes(
156
156
  }
157
157
  }
158
158
 
159
- /// The tail of a free-form encode error: the reshaped `JavaScriptValue` alternative when the type is a
160
- /// container (`[String: Any]` -> `[String: JavaScriptValue]`), otherwise the passthrough suggestion
161
- /// for a bare `Any`. Both conform to the codable protocols, so either is a valid fix.
159
+ /// The tail of a free-form encode error: the typed wrapper of the same shape (`JavaScriptArray`,
160
+ /// `JavaScriptObject`) when there is one, otherwise the plain `JavaScriptValue` passthrough suggestion.
162
161
  private func suggestedAlternative(for freeFormType: String) -> String {
163
162
  if let reshaped = typedFreeFormReplacement(for: freeFormType), reshaped != "JavaScriptValue" {
164
163
  return "Use '\(reshaped)', or 'JavaScriptValue' to pass a JS value through unchanged."
@@ -189,7 +188,7 @@ private func freeFormArgumentWarning(
189
188
  suggesting suggestedType: String
190
189
  ) -> JSDiagnosticMessage {
191
190
  return JSDiagnosticMessage(
192
- "Prefer '\(suggestedType)' over the free-form '\(freeFormType)' for a @JS argument. Free-form decoding boxes every value as 'Any' (slower, no static typing); the 'JavaScriptValue' element keeps each value inspectable without erasing it.",
191
+ "Prefer '\(suggestedType)' over the free-form '\(freeFormType)' for a @JS argument. Free-form decoding boxes every value as 'Any' (slower, no static typing); '\(suggestedType)' keeps the JS value inspectable without converting it.",
193
192
  id: "js-free-form-argument",
194
193
  severity: .warning
195
194
  )
@@ -1,8 +1,15 @@
1
1
  import ExpoModulesScanner
2
- import Foundation
3
2
  import SwiftCompilerPlugin
4
3
  import SwiftSyntaxMacros
5
4
 
5
+ #if canImport(Darwin)
6
+ import Darwin
7
+ #elseif canImport(Glibc)
8
+ import Glibc
9
+ #elseif os(Windows)
10
+ import CRT
11
+ #endif
12
+
6
13
  struct ExpoModulesMacrosPlugin: CompilerPlugin {
7
14
  let providingMacros: [Macro.Type] = [
8
15
  OptimizedFunctionAttachedMacro.self,
@@ -529,7 +529,7 @@ private func toObjectMethod(properties: [RecordProperty], inheritsRecord: Bool)
529
529
  switch property.freeForm {
530
530
  case .shape(_, let encodeMethod, let isOptional, _):
531
531
  encode = isOptional
532
- ? "self.\(property.name) == nil ? .null : try JavaScriptValue.\(encodeMethod)(self.\(property.name)!, in: runtime)"
532
+ ? "self.\(property.name) == nil ? .null() : try JavaScriptValue.\(encodeMethod)(self.\(property.name)!, in: runtime)"
533
533
  : "try JavaScriptValue.\(encodeMethod)(self.\(property.name), in: runtime)"
534
534
  case .generic:
535
535
  // The explicit `as Any` keeps an optional property from warning about its implicit coercion.