# @bacons/apple-targets `@bacons/apple-targets` is an Expo Config Plugin that enables React Native/Expo apps to add native Apple extension targets — Widgets, App Clips, Share Extensions, Safari Extensions, Watch apps, Siri Intents, and many more — using Continuous Native Generation (CNG). Target source files live outside the `/ios` directory, are tracked in version control, and are wired into the Xcode project automatically during `expo prebuild`. The companion CLI `create-target` scaffolds new targets with a single command, generating the `expo-target.config.js` and starter Swift template files. The plugin works by scanning a `targets/` directory for per-target `expo-target.config.js` (or `.json`) files. Each config file describes one extension type along with its icon, colors, entitlements, additional frameworks, and bundle identifier. During prebuild, the plugin creates or updates the Xcode native target, generates `Assets.xcassets` color sets and image sets from the config, writes the `Info.plist`, configures code-signing entitlements, and registers the extension with EAS Build for automatic provisioning. The runtime `ExtensionStorage` class provides a JavaScript bridge for sharing data between the main app and its extensions via App Groups / `UserDefaults`. --- ## APIs and Key Functions ### `withTargetsDir` — Main Expo Config Plugin entry point The default export of `@bacons/apple-targets`. Glob-scans `targets/*/expo-target.config.@(json|js)` and applies a full Xcode integration for every target found. Accepts an optional `root`, `match`, and `appleTeamId` override. This is the only plugin that needs to be registered in `app.json`. ```js // app.json — minimum integration { "expo": { "ios": { "bundleIdentifier": "com.example.myapp", "appleTeamId": "ABCDE12345" }, "plugins": [ // Simplest usage — scan all targets/ sub-directories "@bacons/apple-targets", // With options: [ "@bacons/apple-targets", { "appleTeamId": "ABCDE12345", // override team ID "root": "./targets", // default "match": "*" // glob to filter subdirectories } ] ] } } // After registration run: // npx expo prebuild -p ios ``` --- ### `Config` type — Per-target configuration schema Typed shape accepted by `expo-target.config.js`. Supports a plain object export or a function that receives the root `ExpoConfig` and returns a `Config`. Every field except `type` is optional. ```js // targets/widgets/expo-target.config.js — full example /** @type {import('@bacons/apple-targets/app.plugin').ConfigFunction} */ module.exports = (config) => ({ // Required: one of the ~40 supported ExtensionType values type: "widget", // Human-readable display name (CFBundleDisplayName) name: "My Widget", displayName: "My Widget", // Bundle identifier — prefix with "." to append to the main app's bundle ID bundleIdentifier: ".widgets", // resolves to "com.example.myapp.widgets" // Icon — local path relative to this config file, or a URL icon: "../../assets/icon.png", // icon: "https://github.com/expo.png", // Extra system frameworks beyond the defaults for this target type frameworks: ["CoreLocation"], // iOS deployment target (default: "18.0"; watchOS default: "11.0") deploymentTarget: "17.0", // Apple team ID (falls back to main app's ios.appleTeamId) appleTeamId: "ABCDE12345", // Entitlements written to generated.entitlements entitlements: { "com.apple.security.application-groups": [ `group.${config.ios.bundleIdentifier}.shared`, ], "com.apple.developer.siri": true, }, // Named colors → Assets.xcassets/.colorset // Special names: $accent (ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME) // $widgetBackground (ASSETCATALOG_COMPILER_WIDGET_BACKGROUND_COLOR_NAME) colors: { $accent: "steelblue", $widgetBackground: "dodgerblue", primaryText: { light: "#000000", dark: "#ffffff" }, }, // Named images → Assets.xcassets/.imageset // Value can be a URL, local path, or per-scale object; SVGs are auto-detected // as SF Symbol templates and written as .symbolset instead of .imageset images: { logo: "../../assets/logo.png", // logo: "https://github.com/expo.png", // logo: { "1x": "./logo.png", "2x": "./logo@2x.png", "3x": "./logo@3x.png" }, // sfSymbol: "./star.sfsymbol.svg", // auto-detected SF Symbol → .symbolset }, // Embed the JS bundle so the extension can run React Native code (App Clips, Share Extensions) exportJs: false, // default; true is implied for "clip" type }); ``` --- ### `ExtensionType` — Supported target types Union type derived from `TARGET_REGISTRY`. Every value corresponds to a native Apple extension or application target type. ```ts import type { ExtensionType } from "@bacons/apple-targets"; // All currently registered types: const types: ExtensionType[] = [ "widget", // Home-screen WidgetKit widget (includes Live Activities & Dynamic Island) "watch", // watchOS companion app "watch-widget", // WatchOS face complication via WidgetKit "clip", // App Clip (on-demand-install-capable application) "share", // Share Extension "action", // Action Extension (share sheet headless action) "safari", // Safari Web Extension "intent", // Siri Intent Extension "intent-ui", // Siri Intent UI Extension "app-intent", // AppIntents ExtensionKit extension "notification-content",// Rich notification content extension "notification-service",// Notification service extension (payload mutation) "keyboard", // Custom system keyboard "content-blocker", // Safari content blocker "credentials-provider",// AutoFill credential provider "account-auth", // Sign-in-with-Apple account auth modification "bg-download", // Background asset downloader "device-activity-monitor", // Screen Time device activity monitor "location-push", // Location push service "matter", // Matter device setup "quicklook-thumbnail", // QuickLook thumbnail provider "quicklook-preview", // QuickLook preview extension "spotlight", // Spotlight importer "spotlight-delegate", // CoreSpotlight delegate "file-provider", // File provider (non-UI) "file-provider-ui", // File provider actions UI "broadcast-upload", // ReplayKit broadcast upload "broadcast-setup-ui", // ReplayKit broadcast setup UI "call-directory", // CallKit call directory "message-filter", // SMS/MMS message filter "photo-editing", // Photos editing extension "network-packet-tunnel", // NetworkExtension: packet tunnel "network-app-proxy", // NetworkExtension: app proxy "network-filter-data", // NetworkExtension: filter data "network-dns-proxy", // NetworkExtension: DNS proxy "classkit-context", // ClassKit context provider "unwanted-communication", // IdentityLookup unwanted communication "virtual-conference", // Calendar virtual conference provider "shield-action", // Screen Time shield action "shield-config", // Screen Time shield configuration "print-service", // AirPrint print service "smart-card", // CryptoTokenKit smart card "authentication-services", // AppSSO single sign-on // "imessage", // iMessage sticker pack — no Swift template, excluded from CLI ]; ``` --- ### `ExtensionStorage` — Runtime data bridge between app and extensions JavaScript class that wraps the native `ExtensionStorage` Expo module. Allows the main React Native app to read and write values to a shared `UserDefaults` suite (App Group), which the native extension reads directly in Swift. Call `reloadWidget()` after writing to trigger a WidgetKit timeline refresh. ```ts import { ExtensionStorage } from "@bacons/apple-targets"; // Instantiate with the App Group identifier configured in entitlements const storage = new ExtensionStorage("group.com.example.myapp.shared"); // Write — type is inferred from the value storage.set("userName", "Evan"); // string storage.set("score", 42); // number → setInt storage.set("profile", { name: "Evan", age: 30 }); // object → setObject storage.set("items", [{ id: "1" }, { id: "2" }]); // array → setArray storage.set("staleKey", undefined); // removes the key // Read — always returns string | null (native limitation) const name = storage.get("userName"); // "Evan" const missing = storage.get("unknown"); // null // Remove storage.remove("score"); // Tell WidgetKit to refresh timeline (call after writing shared data) ExtensionStorage.reloadWidget(); // all widgets ExtensionStorage.reloadWidget("MyWidget"); // specific widget by name // Tell ControlCenter to refresh (iOS 18+ controls) ExtensionStorage.reloadControls(); ExtensionStorage.reloadControls("MyControl"); // ── Full usage example ────────────────────────────────────────────────────── async function updateWidgetData(stats: { steps: number; goal: number }) { const storage = new ExtensionStorage("group.com.example.myapp.health"); storage.set("steps", stats.steps); storage.set("goal", stats.goal); storage.set("lastUpdated", new Date().toISOString()); ExtensionStorage.reloadWidget("HealthWidget"); } ``` --- ### `createAsync` — Programmatic target scaffolding (create-target CLI internals) Creates a new target directory under `targets/`, copies Swift template files, writes `expo-target.config.js` and `Info.plist`, and optionally installs `@bacons/apple-targets` via `expo install`. ```ts import { createAsync } from "create-target/src/createAsync"; // Used internally by the CLI — can also be called directly in scripts await createAsync("widget", { install: true }); // Creates: ./targets/widget/expo-target.config.js // ./targets/widget/Info.plist // ./targets/widget/*.swift (from bundled templates) // Modifies: app.json / app.config.js to add "@bacons/apple-targets" plugin // Logs: "Run npx expo prebuild -p ios to fully generate the target." // Equivalent CLI invocation: // npx create-target widget // bunx create-target widget --no-install // yarn create target widget ``` --- ### `create-target` CLI — Interactive target scaffolding The `create-target` CLI is the primary user-facing tool. Run from the Expo project root; it detects the project, prompts for target type if none is given, and wires everything up. ```bash # Interactive — presents a searchable list of all target types bunx create-target # Non-interactive — pass the target type directly bunx create-target widget bunx create-target clip bunx create-target share bunx create-target safari bunx create-target notification-service # Skip auto-installing @bacons/apple-targets bunx create-target widget --no-install # Help bunx create-target --help # Usage: npx create-target [options] # --no-install Skip installing npm packages # -v, --version Version number # -h, --help Usage info ``` --- ### `withIosColorset` — Generate xcassets color sets Expo Config Plugin that writes a `.colorset` directory under `Assets.xcassets` supporting optional dark-mode variants. Used internally by `withWidget` when `colors` are declared in the target config, but can be used directly. ```ts import { withIosColorset } from "@bacons/apple-targets/src/colorset/with-ios-colorset"; // Usage inside a custom config plugin const config = withIosColorset(expoConfig, { cwd: "targets/mywidget", // relative to project root name: "brandPrimary", // produces Assets.xcassets/brandPrimary.colorset color: "#FF6B6B", // light-mode color (any CSS color string) darkColor: "#C0392B", // optional dark-mode color }); // Result: targets/mywidget/Assets.xcassets/brandPrimary.colorset/Contents.json // In Swift: Color("brandPrimary") → #FF6B6B (light) / #C0392B (dark) ``` --- ### `withIosIcon` — Generate extension app icons Generates an `AppIcon.appiconset` inside `Assets.xcassets` for the target at all required resolutions (20pt–1024pt for iOS; 1024pt universal for watchOS). Supports local paths and remote URLs. ```ts import { withIosIcon } from "@bacons/apple-targets/src/icon/with-ios-icon"; const config = withIosIcon(expoConfig, { type: "widget", // ExtensionType — affects generation strategy cwd: "targets/mywidget", // relative to project root iconFilePath: "assets/widget-icon.png", // or "https://example.com/icon.png" isTransparent: false, // true for action extensions }); // Writes: targets/mywidget/Assets.xcassets/AppIcon.appiconset/ // App-Icon-20x20@2x.png, App-Icon-60x60@3x.png, ItunesArtwork@2x.png, … ``` --- ### `withImageAsset` — Generate named image assets Writes a named `.imageset` to `Assets.xcassets`. Accepts a single source (auto-scaled) or explicit per-density sources. Detects SF Symbol SVGs and promotes them to `.symbolset` automatically. ```ts import { withImageAsset } from "@bacons/apple-targets/src/icon/with-image-asset"; // Single source — used at 1x, no 2x/3x generated const config = withImageAsset(expoConfig, { cwd: "targets/mywidget", name: "companyLogo", image: "https://github.com/expo.png", }); // Per-scale sources const config2 = withImageAsset(expoConfig, { cwd: "targets/mywidget", name: "hero", image: { "1x": "./hero.png", "2x": "./hero@2x.png", "3x": "./hero@3x.png" }, }); // In Swift: Image("companyLogo") → companyLogo.imageset contents ``` --- ### `isSFSymbolContent` — Detect SF Symbol SVG templates Heuristic that checks whether SVG text content is a multi-weight SF Symbol template (contains `id="Symbols"` and `id="Regular-S"` groups). Used internally to decide between writing a `.symbolset` vs. a plain `.imageset`. ```ts import { isSFSymbolContent } from "@bacons/apple-targets/src/symbolset/with-ios-symbolset"; const svgContent = await fs.promises.readFile("./star.svg", "utf-8"); if (isSFSymbolContent(svgContent)) { console.log("This is an SF Symbol template → write .symbolset"); } else { console.log("Regular SVG → write .imageset"); } // In expo-target.config.js images, .svg files are auto-detected: // images: { myIcon: "./star.svg" } // → .symbolset (if SF Symbol) → Swift: Image("myIcon") // → .imageset (regular SVG) → Swift: Image("myIcon") ``` --- ### `withEASTargets` — Register targets for EAS Build code signing Injects extension metadata into `extra.eas.build.experimental.ios.appExtensions` so EAS Build can provision and sign the target automatically. ```ts import { withEASTargets } from "@bacons/apple-targets/src/with-eas-credentials"; // Automatically called by withWidget — exposed for advanced use cases const config = withEASTargets(expoConfig, { bundleIdentifier: "com.example.myapp.widgets", targetName: "widgets", entitlements: { "com.apple.security.application-groups": ["group.com.example.myapp.shared"], }, }); // Resulting extra.eas.build.experimental.ios.appExtensions entry: // { // "bundleIdentifier": "com.example.myapp.widgets", // "targetName": "widgets", // "entitlements": { "com.apple.security.application-groups": ["group.com.example.myapp.shared"] } // } ``` --- ### `withPodTargetExtension` — Load per-target `pods.rb` files Appends a Ruby snippet to the Podfile that globs `targets/**/pods.rb` and evaluates each file inside the matching CocoaPods target block. Allows targets with native dependencies (e.g. React Native-based App Clips) to declare pods without editing the root Podfile. ```ruby # targets/clip/pods.rb — evaluated inside `target "clip" do … end` pod 'React-Core', :path => '../node_modules/react-native/' pod 'RNCAsyncStorage', :path => '../node_modules/@react-native-async-storage/async-storage' ``` ```ts import { withPodTargetExtension } from "@bacons/apple-targets/src/with-pod-target-extension"; // Applied automatically by withTargetsDir; call directly only in custom plugins const config = withPodTargetExtension(expoConfig); ``` --- ### `TARGET_REGISTRY` — Central extension type metadata Single source of truth for all 40+ extension types. Every derived map, CLI list, and e2e test registry is built from this object. ```ts import { TARGET_REGISTRY, type TargetDefinition } from "@bacons/apple-targets/src/target"; // Inspect a specific entry const widgetDef: TargetDefinition = TARGET_REGISTRY["widget"]; // { // extensionPointIdentifier: "com.apple.widgetkit-extension", // frameworks: ["WidgetKit", "SwiftUI", "ActivityKit", "AppIntents"], // appGroupsByDefault: true, // displayName: "Widget", // description: "Home screen widget" // } // Helper functions derived from the registry import { productTypeForType, needsEmbeddedSwift, getFrameworksForType, isNativeTargetOfType, getMainAppTarget, getAuxiliaryTargets, } from "@bacons/apple-targets/src/target"; productTypeForType("clip"); // "com.apple.product-type.application.on-demand-install-capable" productTypeForType("widget"); // "com.apple.product-type.app-extension" needsEmbeddedSwift("share"); // true needsEmbeddedSwift("widget"); // false getFrameworksForType("widget"); // ["WidgetKit", "SwiftUI", "ActivityKit", "AppIntents"] getFrameworksForType("call-directory"); // ["CallKit"] ``` --- ### `customColorFromCSS` — Parse CSS color strings to Apple float components Converts any CSS color string (named color, hex, `rgb()`, `hsl()`, etc.) into the `{red, green, blue, alpha}` float tuple required by Xcode's `Assets.xcassets` colorset `Contents.json`. ```ts import { customColorFromCSS } from "@bacons/apple-targets/src/colorset/custom-color-from-css"; customColorFromCSS("#FF6B6B"); // { red: 1, green: 0.4196..., blue: 0.4196..., alpha: 1 } customColorFromCSS("steelblue"); // { red: 0.2745..., green: 0.5098..., blue: 0.7059..., alpha: 1 } customColorFromCSS("rgba(255, 0, 0, 0.5)"); // { red: 1, green: 0, blue: 0, alpha: 0.5 } ``` --- ## Summary `@bacons/apple-targets` is the go-to solution for Expo/React Native apps that need native iOS extension targets without abandoning the managed workflow. The most common use case is adding a **WidgetKit widget** (home screen or Dynamic Island) backed by shared data from the main app: create a `targets/widget/expo-target.config.js` declaring `type: "widget"`, set `colors.$accent` and `colors.$widgetBackground`, use `ExtensionStorage` in JavaScript to push data into the App Group, and call `ExtensionStorage.reloadWidget()` to trigger a timeline refresh. Other high-frequency patterns include **App Clips** for frictionless onboarding (automatic `clip` bundle ID suffix, associated-domain forwarding from the main app, React Native JS bundle embedding via `exportJs: true`), **Share Extensions** to receive content from other apps, and **Notification Service** or **Notification Content** extensions for rich push notifications — all configured with a few lines in their respective `expo-target.config.js` files. Integration with CI/CD is handled end-to-end: `withEASTargets` automatically registers every target with EAS Build's `appExtensions` for code-signing, while `withPodTargetExtension` lets each target declare its own CocoaPods dependencies via a `pods.rb` file. The `create-target` CLI (`bunx create-target `) accelerates onboarding by scaffolding the directory, generating an `expo-target.config.js` with correct defaults and recommended entitlements, copying the Swift template source, and ensuring `@bacons/apple-targets` is installed and registered in `app.json`. A single `npx expo prebuild -p ios` then wires everything into the Xcode project, making the entire flow compatible with Expo's Continuous Native Generation model — no manual Xcode edits required.