@expo/expo-modules-macros-plugin

中文文档
0.13.1发布于 2026/09/26MITTypeScriptCommonJS零依赖构建来源已验证近 30 天下载 2476.6万

Swift macro plugin for Expo modules

原始使用文档

expo-modules-macros-plugin

@expo/expo-modules-macros-plugin is the Swift compiler plugin behind the Expo Modules API. It implements the macros that expo-modules-core declares, so a module author writes plain Swift declarations and the plugin synthesizes the code that binds them to JavaScript.

The same executable doubles as a source scanner CLI.

Installation

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-tool, so consumers never build the plugin themselves.

Macros

  • @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.
  • @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.
  • @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.
  • @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:).
  • @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.
  • @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.

How that looks in a module:

import ExpoModulesCore

@ExpoModule(classes: [Cache.self])
public final class MyModule {
  @JS
  func greet(name: String) -> String {
    "Hi, \(name)"
  }

  @JS("doWork")
  func performWork() async throws { ... }

  @Event
  var onProgress: (ProgressEvent) -> Void
}

@SharedObject
final class Cache: SharedObject {
  @JS
  init(name: String) { ... }

  @JS
  func get(_ key: String) -> String? { ... }
}

The author-facing documentation for each macro lives next to its declaration in expo-modules-core, in ios/Core/ExpoModulesMacros.swift. This repository holds the implementations.

Scanner

The Swift compiler launches a plugin executable with no arguments and speaks the plugin protocol over stdin, so the binary treats any argument as a scanner invocation instead:

ExpoModulesMacros-tool <subcommand> [options] <path> [<path> ...]

subcommands:
  scan-modules   fast scan for top-level @ExpoModule types (autolinking)
  scan-exports   deep scan of the full JS-exported surface (type generation)

options (scan-modules only):
  --platform <os>   evaluate '#if os(...)' against this platform
  --define <flag>   treat a conditional compilation flag as set; repeatable

Each path is a .swift file or a directory, scanned recursively for .swift files. Both subcommands print a JSON report to stdout, each carrying its own schemaVersion so a consumer can check it understands the shape before trusting it. The two versions are independent: the commands serve different consumers and change for different reasons.

TypeScript wrapper

Node consumers can call the scanner through this package instead of locating the binary and shelling out themselves:

import { scanModules, scanExports } from '@expo/expo-modules-macros-plugin';

const { modules, warnings } = await scanModules(['ios/'], { defines: ['DEBUG'] });
const { exports } = await scanExports(['ios/']);

The binary is a compiled executable, so each call still spawns a process. What the wrapper owns is the part consumers would otherwise duplicate: resolving the shipped binary, building the arguments, parsing the JSON, checking schemaVersion, and turning a non-zero exit into a ScannerError. The result types are hand-written mirrors of the Swift Codable types, which is what the version check guards against drifting.

How the plugin reaches the compiler

expo-modules-core declares the macro signatures with #externalMacro(module: "ExpoModulesMacros", type: …). During pod install, expo-modules-autolinking resolves this package from the core package and appends

-Xfrontend -load-plugin-executable -Xfrontend <plugin>/apple/ExpoModulesMacros-tool#ExpoModulesMacros

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.

The module and type names in #externalMacro must stay in sync with apple/Sources/ExpoModulesMacros/Plugin.swift.

Development

Requires macOS 13 or newer and a toolchain with Swift 6.2, which means Xcode 26 or newer.

cd apple
swift build
swift test

npm run build runs apple/build.js, which builds the release binary for arm64 and x86_64, merges the slices into apple/ExpoModulesMacros-tool 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.

Releasing

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.

Contributing

Contributions are very welcome! Please refer to the guidelines described in the contributing guide.

版本与发布记录

@expo/expo-modules-macros-plugin 共发布 22 个版本,首次发布于 2026/03/28;过去 12 个月发布了 22 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.13.1latest2026/09/261 个月前
0.13.02026/09/261 个月前
0.12.02026/09/181 个月前
0.11.02026/09/171 个月前
0.10.02026/09/092 个月前
0.9.02026/09/062 个月前

关于 @expo/expo-modules-macros-plugin 的常见问题

@expo/expo-modules-macros-plugin 是什么?

Swift macro plugin for Expo modules

如何安装 @expo/expo-modules-macros-plugin?

在项目目录运行 npm install @expo/expo-modules-macros-plugin,或使用 pnpm add / yarn add / bun add。

@expo/expo-modules-macros-plugin 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@expo/expo-modules-macros-plugin 支持 ESM 还是 CommonJS?

以 CommonJS 形式发布,可以直接 require();在 ES Module 项目中也可以用 import 引入(Node.js 会把 module.exports 作为默认导出)。

@expo/expo-modules-macros-plugin 可以商用吗?

@expo/expo-modules-macros-plugin 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@expo/expo-modules-macros-plugin 的下载量有多少?

根据 npm 官方下载统计,@expo/expo-modules-macros-plugin 在 2026/10/14 – 2026/11/12 这 30 天内被下载 24,766,436 次,日均约 825,548 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@expo/expo-modules-macros-plugin 还在维护吗?

最新版本 0.13.1 发布于 2026/09/26(1 个月前);至今共发布 22 个版本,其中过去 12 个月发布了 22 个;首次发布于 2026/03/28。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。