@macify/xpc 0.0.2 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require("./plugin/build/index.cjs");
@@ -0,0 +1,33 @@
1
+ //#region src/index.d.ts
2
+ type EventHandler = (data: any) => void;
3
+ /**
4
+ * Client API for communicating with the XPC service.
5
+ */
6
+ declare const macx: {
7
+ /**
8
+ * Call the XPC service with an RPC message.
9
+ * @param type - The message type (e.g. "ping", "processImage")
10
+ * @param payload - The payload object (will be JSON-serialized)
11
+ * @returns The response from the XPC service
12
+ */
13
+ call<T = any>(type: string, payload?: Record<string, any>): Promise<T>;
14
+ /**
15
+ * Listen for events pushed from the XPC service, or connection state changes.
16
+ *
17
+ * XPC events: `macx.on("progress", (data) => { ... })`
18
+ * Connection state: `macx.on("connectionLost", () => { ... })`
19
+ * `macx.on("connectionRestored", () => { ... })`
20
+ *
21
+ * @param type - The event type to listen for
22
+ * @param handler - Callback invoked with the event payload
23
+ * @returns Unsubscribe function
24
+ */
25
+ on(type: string, handler: EventHandler): () => void;
26
+ /**
27
+ * Remove all listeners for a specific event type.
28
+ * For fine-grained control, use the unsubscribe function returned by `on()`.
29
+ */
30
+ off(type: string): void;
31
+ };
32
+ //#endregion
33
+ export { macx };
@@ -0,0 +1,6 @@
1
+ {
2
+ "platforms": ["apple"],
3
+ "apple": {
4
+ "modules": ["MacifyXPCModule"]
5
+ }
6
+ }
@@ -0,0 +1,152 @@
1
+ import Foundation
2
+
3
+ /// Client that connects to the XPC service from the main app process.
4
+ /// Provides generic JSON RPC and receives events from the XPC service.
5
+ class MacifyServiceClient: NSObject, MacifyXPCEventReceiver {
6
+ static let shared = MacifyServiceClient()
7
+
8
+ private var connection: NSXPCConnection?
9
+ private var serviceName: String?
10
+ private var isConnected = false
11
+
12
+ /// Callback for events received from the XPC service.
13
+ /// Set by MacifyXPCModule to bridge events to JS.
14
+ var onEvent: ((String, [String: Any]) -> Void)?
15
+
16
+ /// Callback for connection state changes (lost/restored).
17
+ /// Set by MacifyXPCModule to bridge connection events to JS.
18
+ var onConnectionStateChange: ((ConnectionState) -> Void)?
19
+
20
+ enum ConnectionState {
21
+ case lost
22
+ case restored
23
+ }
24
+
25
+ /// Configure the service name. Called once from MacifyXPCModule on init.
26
+ func configure(serviceName: String) {
27
+ self.serviceName = serviceName
28
+ }
29
+
30
+ private func getConnection() -> NSXPCConnection? {
31
+ if let existing = connection {
32
+ return existing
33
+ }
34
+
35
+ guard let name = serviceName else {
36
+ return nil
37
+ }
38
+
39
+ // NSXPCConnection(serviceName:) is unavailable in Mac Catalyst Swift headers,
40
+ // but the API exists at runtime on macOS. We use an ObjC bridge to bypass
41
+ // the compile-time availability check.
42
+ let conn = XPCConnectionFactory.connection(withServiceName: name)
43
+
44
+ // Interface the XPC service exports (we call it)
45
+ conn.remoteObjectInterface = NSXPCInterface(
46
+ with: MacifyXPCProtocol.self
47
+ )
48
+
49
+ // Interface we export (XPC service calls us for events)
50
+ conn.exportedInterface = NSXPCInterface(
51
+ with: MacifyXPCEventReceiver.self
52
+ )
53
+ conn.exportedObject = self
54
+
55
+ conn.invalidationHandler = { [weak self] in
56
+ guard let self = self else { return }
57
+ self.connection = nil
58
+ if self.isConnected {
59
+ self.isConnected = false
60
+ self.onConnectionStateChange?(.lost)
61
+ }
62
+ }
63
+
64
+ conn.interruptionHandler = { [weak self] in
65
+ guard let self = self else { return }
66
+ self.connection = nil
67
+ if self.isConnected {
68
+ self.isConnected = false
69
+ self.onConnectionStateChange?(.lost)
70
+ }
71
+ }
72
+
73
+ conn.resume()
74
+ connection = conn
75
+ isConnected = true
76
+
77
+ return conn
78
+ }
79
+
80
+ /// Generic RPC call to the XPC service.
81
+ func call(type: String, payload: [String: Any]) async throws -> [String: Any] {
82
+ guard let conn = getConnection() else {
83
+ throw NSError(
84
+ domain: "MacifyXPC",
85
+ code: -3,
86
+ userInfo: [NSLocalizedDescriptionKey: "XPC connection was lost — service name not configured"]
87
+ )
88
+ }
89
+
90
+ // If we're reconnecting after a lost connection, notify JS
91
+ if !isConnected {
92
+ isConnected = true
93
+ onConnectionStateChange?(.restored)
94
+ }
95
+
96
+ // Encode payload to JSON Data
97
+ let payloadData: Data
98
+ if payload.isEmpty {
99
+ payloadData = Data()
100
+ } else {
101
+ payloadData = try JSONSerialization.data(withJSONObject: payload, options: [])
102
+ }
103
+
104
+ return try await withCheckedThrowingContinuation { continuation in
105
+ let proxy = conn.remoteObjectProxyWithErrorHandler { error in
106
+ continuation.resume(throwing: error)
107
+ }
108
+ guard let service = proxy as? MacifyXPCProtocol else {
109
+ continuation.resume(
110
+ throwing: NSError(
111
+ domain: "MacifyXPC",
112
+ code: -1,
113
+ userInfo: [NSLocalizedDescriptionKey: "XPC connection was lost"]
114
+ )
115
+ )
116
+ return
117
+ }
118
+
119
+ service.call(type: type, payload: payloadData) { responseData, error in
120
+ if let error = error {
121
+ continuation.resume(throwing: error)
122
+ return
123
+ }
124
+ guard let data = responseData else {
125
+ continuation.resume(returning: [:])
126
+ return
127
+ }
128
+ do {
129
+ let result = try JSONSerialization.jsonObject(with: data, options: [])
130
+ continuation.resume(returning: (result as? [String: Any]) ?? [:])
131
+ } catch {
132
+ continuation.resume(throwing: error)
133
+ }
134
+ }
135
+ }
136
+ }
137
+
138
+ // MARK: - MacifyXPCEventReceiver
139
+
140
+ /// Called by the XPC service when it pushes an event to the app.
141
+ func handleEvent(type: String, payload: Data) {
142
+ let decoded: [String: Any]
143
+ if payload.isEmpty {
144
+ decoded = [:]
145
+ } else if let json = try? JSONSerialization.jsonObject(with: payload, options: []) as? [String: Any] {
146
+ decoded = json
147
+ } else {
148
+ decoded = [:]
149
+ }
150
+ onEvent?(type, decoded)
151
+ }
152
+ }
@@ -0,0 +1,18 @@
1
+ Pod::Spec.new do |s|
2
+ s.name = 'MacifyXPC'
3
+ s.version = '0.0.1'
4
+ s.summary = 'XPC service client for Mac Catalyst Expo apps'
5
+ s.description = 'Provides XPC communication between React Native and a macOS XPC service'
6
+ s.homepage = 'https://github.com/user/macify'
7
+ s.license = 'MIT'
8
+ s.author = 'Theo'
9
+ s.source = { git: '' }
10
+
11
+ s.platform = :ios, '15.1'
12
+ s.swift_version = '5.9'
13
+ s.static_framework = true
14
+
15
+ s.dependency 'ExpoModulesCore'
16
+
17
+ s.source_files = '**/*.{h,m,swift}'
18
+ end
@@ -0,0 +1,85 @@
1
+ import ExpoModulesCore
2
+
3
+ public class MacifyXPCModule: Module {
4
+ private var hasListeners = false
5
+
6
+ public func definition() -> ModuleDefinition {
7
+ Name("MacifyXPC")
8
+
9
+ // Read the service name from Info.plist (set by the config plugin)
10
+ OnCreate {
11
+ if let serviceName = Bundle.main.infoDictionary?["MacifyXPCServiceName"] as? String {
12
+ MacifyServiceClient.shared.configure(serviceName: serviceName)
13
+ }
14
+ }
15
+
16
+ // Only bridge events to JS when there are active listeners
17
+ OnStartObserving {
18
+ self.hasListeners = true
19
+
20
+ // Bridge XPC events to JS
21
+ MacifyServiceClient.shared.onEvent = { [weak self] type, payload in
22
+ guard let self = self, self.hasListeners else { return }
23
+ if let data = try? JSONSerialization.data(withJSONObject: payload, options: []),
24
+ let jsonString = String(data: data, encoding: .utf8) {
25
+ self.sendEvent("onXPCEvent", [
26
+ "type": type,
27
+ "payload": jsonString,
28
+ ])
29
+ }
30
+ }
31
+
32
+ // Bridge connection state changes to JS
33
+ MacifyServiceClient.shared.onConnectionStateChange = { [weak self] state in
34
+ guard let self = self, self.hasListeners else { return }
35
+ switch state {
36
+ case .lost:
37
+ self.sendEvent("onXPCConnectionState", [
38
+ "type": "connectionLost",
39
+ "payload": "",
40
+ ])
41
+ case .restored:
42
+ self.sendEvent("onXPCConnectionState", [
43
+ "type": "connectionRestored",
44
+ "payload": "",
45
+ ])
46
+ }
47
+ }
48
+ }
49
+
50
+ OnStopObserving {
51
+ self.hasListeners = false
52
+ MacifyServiceClient.shared.onEvent = nil
53
+ MacifyServiceClient.shared.onConnectionStateChange = nil
54
+ }
55
+
56
+ Events("onXPCEvent", "onXPCConnectionState")
57
+
58
+ /// Generic RPC call to the XPC service.
59
+ /// Takes a type string and a JSON-encoded payload string.
60
+ /// Returns a JSON-encoded response string.
61
+ AsyncFunction("call") { (type: String, payloadJSON: String) -> String in
62
+ // Decode the JSON payload string into a dictionary
63
+ let payload: [String: Any]
64
+ if payloadJSON.isEmpty || payloadJSON == "{}" {
65
+ payload = [:]
66
+ } else {
67
+ guard let data = payloadJSON.data(using: .utf8),
68
+ let decoded = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
69
+ throw NSError(
70
+ domain: "MacifyXPC",
71
+ code: -2,
72
+ userInfo: [NSLocalizedDescriptionKey: "Invalid JSON payload"]
73
+ )
74
+ }
75
+ payload = decoded
76
+ }
77
+
78
+ let result = try await MacifyServiceClient.shared.call(type: type, payload: payload)
79
+
80
+ // Encode response back to JSON string for JS
81
+ let responseData = try JSONSerialization.data(withJSONObject: result, options: [])
82
+ return String(data: responseData, encoding: .utf8) ?? "{}"
83
+ }
84
+ }
85
+ }
@@ -0,0 +1,16 @@
1
+ import Foundation
2
+
3
+ /// Wire protocol for RPC calls from the app to the XPC service.
4
+ /// This is the app-side copy — the XPC service has its own copy in the SPM package.
5
+ @objc protocol MacifyXPCProtocol {
6
+ func call(
7
+ type: String,
8
+ payload: Data,
9
+ withReply reply: @escaping (Data?, NSError?) -> Void
10
+ )
11
+ }
12
+
13
+ /// Reverse channel: the XPC service pushes events back to the app.
14
+ @objc protocol MacifyXPCEventReceiver {
15
+ func handleEvent(type: String, payload: Data)
16
+ }
@@ -0,0 +1,14 @@
1
+ #import <Foundation/Foundation.h>
2
+
3
+ NS_ASSUME_NONNULL_BEGIN
4
+
5
+ /// Bridges NSXPCConnection(serviceName:) which is unavailable in Mac Catalyst
6
+ /// Swift headers. The API exists at runtime on macOS — it's only blocked at
7
+ /// compile time because Catalyst uses the iOS SDK surface.
8
+ @interface XPCConnectionFactory : NSObject
9
+
10
+ + (NSXPCConnection *)connectionWithServiceName:(NSString *)serviceName;
11
+
12
+ @end
13
+
14
+ NS_ASSUME_NONNULL_END
@@ -0,0 +1,17 @@
1
+ #import "XPCConnectionFactory.h"
2
+ #import <objc/message.h>
3
+
4
+ @implementation XPCConnectionFactory
5
+
6
+ + (NSXPCConnection *)connectionWithServiceName:(NSString *)serviceName {
7
+ // NSXPCConnection(serviceName:) is marked API_UNAVAILABLE(ios) which
8
+ // blocks compilation in Mac Catalyst even though it exists at runtime.
9
+ // Use objc_msgSend to bypass the compile-time availability check.
10
+ NSXPCConnection *conn = [NSXPCConnection alloc];
11
+ NSXPCConnection *(*sendTyped)(id, SEL, NSString *) =
12
+ (NSXPCConnection *(*)(id, SEL, NSString *))objc_msgSend;
13
+ conn = sendTyped(conn, NSSelectorFromString(@"initWithServiceName:"), serviceName);
14
+ return conn;
15
+ }
16
+
17
+ @end
package/package.json CHANGED
@@ -1,9 +1,18 @@
1
1
  {
2
2
  "name": "@macify/xpc",
3
- "version": "0.0.2",
3
+ "version": "0.0.3",
4
4
  "description": "XPC service support for Mac Catalyst Expo apps",
5
5
  "main": "build/index.cjs",
6
6
  "types": "build/index.d.cts",
7
+ "files": [
8
+ "build/",
9
+ "plugin/build/",
10
+ "ios/",
11
+ "swift/Package.swift",
12
+ "swift/Sources/",
13
+ "app.plugin.js",
14
+ "expo-module.config.json"
15
+ ],
7
16
  "license": "MIT",
8
17
  "scripts": {
9
18
  "build": "tsdown"
@@ -0,0 +1,431 @@
1
+ //#region \0rolldown/runtime.js
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __copyProps = (to, from, except, desc) => {
9
+ if (from && typeof from === "object" || typeof from === "function") {
10
+ for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) {
13
+ __defProp(to, key, {
14
+ get: ((k) => from[k]).bind(null, key),
15
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
16
+ });
17
+ }
18
+ }
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
23
+ value: mod,
24
+ enumerable: true
25
+ }) : target, mod));
26
+
27
+ //#endregion
28
+ let _expo_config_plugins = require("@expo/config-plugins");
29
+ let node_fs = require("node:fs");
30
+ node_fs = __toESM(node_fs);
31
+ let node_path = require("node:path");
32
+ node_path = __toESM(node_path);
33
+
34
+ //#region plugin/src/index.ts
35
+ const XPC_PRODUCT_TYPE = "com.apple.product-type.xpc-service";
36
+ const XPC_PRODUCT_FILE_TYPE = "wrapper.xpc-service";
37
+ const SPM_PRODUCT_NAME = "MacifyXPCService";
38
+ function getServiceName(config, props) {
39
+ if (props.serviceName) return props.serviceName;
40
+ return `${(config.name || "App").replace(/\s+/g, "")}Service`;
41
+ }
42
+ function getBundleId(config, serviceName) {
43
+ const appBundleId = config.ios?.bundleIdentifier;
44
+ if (!appBundleId) throw new Error("[xpc] No ios.bundleIdentifier set in Expo config. Required for XPC service.");
45
+ return `${appBundleId}.${serviceName}`;
46
+ }
47
+ /** Resolve the absolute path to @macify/xpc/swift/ */
48
+ function resolveSwiftPackagePath() {
49
+ const swiftDir = node_path.default.resolve(__dirname, "../../swift");
50
+ if (!node_fs.default.existsSync(swiftDir)) throw new Error(`[xpc] Could not find Swift package at ${swiftDir}. Is @macify/xpc installed?`);
51
+ return swiftDir;
52
+ }
53
+ /** Find the user's xpc-sources directory */
54
+ function findXPCSourcesDir(projectRoot) {
55
+ const xpcSourcesDir = node_path.default.join(projectRoot, "modules", "xpc-service", "xpc-sources");
56
+ if (!node_fs.default.existsSync(xpcSourcesDir)) throw new Error(`[xpc] Could not find XPC sources at ${xpcSourcesDir}. Run 'npx macx init' first.`);
57
+ return xpcSourcesDir;
58
+ }
59
+ const withXPCService = (config, props = {}) => {
60
+ const serviceName = getServiceName(config, props);
61
+ const bundleId = getBundleId(config, serviceName);
62
+ const macosTarget = props.macosDeploymentTarget || "13.0";
63
+ config = (0, _expo_config_plugins.withInfoPlist)(config, (config) => {
64
+ config.modResults.MacifyXPCServiceName = bundleId;
65
+ return config;
66
+ });
67
+ config = (0, _expo_config_plugins.withDangerousMod)(config, ["ios", async (config) => {
68
+ const iosDir = config.modRequest.platformProjectRoot;
69
+ const destDir = node_path.default.join(iosDir, serviceName);
70
+ const xpcSourcesDir = findXPCSourcesDir(config.modRequest.projectRoot);
71
+ if (node_fs.default.existsSync(destDir)) node_fs.default.rmSync(destDir, { recursive: true });
72
+ node_fs.default.mkdirSync(destDir, { recursive: true });
73
+ const files = node_fs.default.readdirSync(xpcSourcesDir);
74
+ for (const file of files) {
75
+ const src = node_path.default.join(xpcSourcesDir, file);
76
+ const dst = node_path.default.join(destDir, file);
77
+ if (node_fs.default.statSync(src).isFile()) node_fs.default.copyFileSync(src, dst);
78
+ }
79
+ console.log(`[xpc] Copied ${files.length} files to ios/${serviceName}/`);
80
+ return config;
81
+ }]);
82
+ config = (0, _expo_config_plugins.withXcodeProject)(config, async (config) => {
83
+ const project = config.modResults;
84
+ if (!config.ios?.appleTeamId) throw new Error("[xpc] No appleTeamId set in Expo config. Required for XPC service signing.");
85
+ const teamId = config.ios.appleTeamId;
86
+ if (project.pbxTargetByName(serviceName)) {
87
+ console.log(`[xpc] Target "${serviceName}" already exists, skipping.`);
88
+ return config;
89
+ }
90
+ const objects = project.hash.project.objects;
91
+ const swiftPackagePath = resolveSwiftPackagePath();
92
+ const iosDir = config.modRequest.platformProjectRoot;
93
+ const relativeSwiftPath = node_path.default.relative(iosDir, swiftPackagePath);
94
+ const spmRefUuid = project.generateUuid();
95
+ const spmRefSection = objects["XCLocalSwiftPackageReference"] || (objects["XCLocalSwiftPackageReference"] = {});
96
+ spmRefSection[spmRefUuid] = {
97
+ isa: "XCLocalSwiftPackageReference",
98
+ relativePath: relativeSwiftPath
99
+ };
100
+ spmRefSection[`${spmRefUuid}_comment`] = "XCLocalSwiftPackageReference \"MacifyXPCService\"";
101
+ const projectSection = project.pbxProjectSection();
102
+ for (const key of Object.keys(projectSection)) if (!key.endsWith("_comment")) {
103
+ const proj = projectSection[key];
104
+ if (!proj.packageReferences) proj.packageReferences = [];
105
+ proj.packageReferences.push({
106
+ value: spmRefUuid,
107
+ comment: "XCLocalSwiftPackageReference \"MacifyXPCService\""
108
+ });
109
+ }
110
+ const spmProductDepUuid = project.generateUuid();
111
+ const spmProductSection = objects["XCSwiftPackageProductDependency"] || (objects["XCSwiftPackageProductDependency"] = {});
112
+ spmProductSection[spmProductDepUuid] = {
113
+ isa: "XCSwiftPackageProductDependency",
114
+ package: spmRefUuid,
115
+ package_comment: "XCLocalSwiftPackageReference \"MacifyXPCService\"",
116
+ productName: SPM_PRODUCT_NAME
117
+ };
118
+ spmProductSection[`${spmProductDepUuid}_comment`] = SPM_PRODUCT_NAME;
119
+ const remoteProductDeps = [];
120
+ if (props.dependencies && props.dependencies.length > 0) {
121
+ const remoteRefSection = objects["XCRemoteSwiftPackageReference"] || (objects["XCRemoteSwiftPackageReference"] = {});
122
+ for (const dep of props.dependencies) {
123
+ if (!dep.version && !dep.exactVersion && !dep.branch) throw new Error(`[xpc] Remote dependency "${dep.url}" must specify one of: version, exactVersion, or branch.`);
124
+ let requirement;
125
+ if (dep.branch) requirement = {
126
+ kind: "branch",
127
+ branch: dep.branch
128
+ };
129
+ else if (dep.exactVersion) requirement = {
130
+ kind: "exactVersion",
131
+ version: dep.exactVersion
132
+ };
133
+ else requirement = {
134
+ kind: "upToNextMajorVersion",
135
+ minimumVersion: dep.version
136
+ };
137
+ const repoName = dep.url.replace(/\.git$/, "").split("/").pop() || dep.url;
138
+ const remoteRefUuid = project.generateUuid();
139
+ remoteRefSection[remoteRefUuid] = {
140
+ isa: "XCRemoteSwiftPackageReference",
141
+ repositoryURL: dep.url,
142
+ requirement
143
+ };
144
+ remoteRefSection[`${remoteRefUuid}_comment`] = `XCRemoteSwiftPackageReference "${repoName}"`;
145
+ for (const key of Object.keys(projectSection)) if (!key.endsWith("_comment")) projectSection[key].packageReferences.push({
146
+ value: remoteRefUuid,
147
+ comment: `XCRemoteSwiftPackageReference "${repoName}"`
148
+ });
149
+ for (const productName of dep.products) {
150
+ const productDepUuid = project.generateUuid();
151
+ spmProductSection[productDepUuid] = {
152
+ isa: "XCSwiftPackageProductDependency",
153
+ package: remoteRefUuid,
154
+ package_comment: `XCRemoteSwiftPackageReference "${repoName}"`,
155
+ productName
156
+ };
157
+ spmProductSection[`${productDepUuid}_comment`] = productName;
158
+ remoteProductDeps.push({
159
+ productDepUuid,
160
+ productName
161
+ });
162
+ }
163
+ console.log(`[xpc] Added remote SPM dependency: ${repoName} (${dep.products.join(", ")})`);
164
+ }
165
+ }
166
+ const sharedBuildSettings = {
167
+ SDKROOT: "macosx",
168
+ PRODUCT_NAME: `"$(TARGET_NAME)"`,
169
+ PRODUCT_BUNDLE_IDENTIFIER: `"${bundleId}"`,
170
+ INFOPLIST_FILE: `"${serviceName}/Info.plist"`,
171
+ SKIP_INSTALL: "YES",
172
+ MACOSX_DEPLOYMENT_TARGET: macosTarget,
173
+ DEVELOPMENT_TEAM: `"${teamId}"`,
174
+ CODE_SIGN_STYLE: "Automatic",
175
+ SWIFT_VERSION: "5.0",
176
+ COMBINE_HIDPI_IMAGES: "YES",
177
+ CLANG_ENABLE_MODULES: "YES",
178
+ SWIFT_OPTIMIZATION_LEVEL: "\"-Onone\""
179
+ };
180
+ const debugBuildSettings = {
181
+ ...sharedBuildSettings,
182
+ DEBUG_INFORMATION_FORMAT: "\"dwarf\"",
183
+ GCC_OPTIMIZATION_LEVEL: "\"0\"",
184
+ SWIFT_ACTIVE_COMPILATION_CONDITIONS: "\"$(inherited) DEBUG\"",
185
+ MTL_ENABLE_DEBUG_INFO: "INCLUDE_SOURCE"
186
+ };
187
+ const releaseBuildSettings = {
188
+ ...sharedBuildSettings,
189
+ DEBUG_INFORMATION_FORMAT: "\"dwarf-with-dsym\"",
190
+ SWIFT_OPTIMIZATION_LEVEL: "\"-O\"",
191
+ COPY_PHASE_STRIP: "YES",
192
+ ENABLE_NS_ASSERTIONS: "NO"
193
+ };
194
+ const configList = project.addXCConfigurationList([{
195
+ name: "Debug",
196
+ isa: "XCBuildConfiguration",
197
+ buildSettings: debugBuildSettings
198
+ }, {
199
+ name: "Release",
200
+ isa: "XCBuildConfiguration",
201
+ buildSettings: releaseBuildSettings
202
+ }], "Release", `Build configuration list for PBXNativeTarget "${serviceName}"`);
203
+ const productFileRefUuid = project.generateUuid();
204
+ objects.PBXFileReference[productFileRefUuid] = {
205
+ isa: "PBXFileReference",
206
+ explicitFileType: XPC_PRODUCT_FILE_TYPE,
207
+ includeInIndex: 0,
208
+ path: `${serviceName}.xpc`,
209
+ sourceTree: "BUILT_PRODUCTS_DIR",
210
+ name: `${serviceName}.xpc`
211
+ };
212
+ objects.PBXFileReference[`${productFileRefUuid}_comment`] = `${serviceName}.xpc`;
213
+ const productsGroupKey = project.findPBXGroupKey({ name: "Products" });
214
+ if (productsGroupKey) project.addToPbxGroup({
215
+ fileRef: productFileRefUuid,
216
+ basename: `${serviceName}.xpc`
217
+ }, productsGroupKey);
218
+ const xpcSourcesDir = findXPCSourcesDir(config.modRequest.projectRoot);
219
+ const swiftFiles = node_fs.default.readdirSync(xpcSourcesDir).filter((f) => node_fs.default.statSync(node_path.default.join(xpcSourcesDir, f)).isFile()).filter((f) => f.endsWith(".swift"));
220
+ const sourceFileRefs = [];
221
+ for (const fileName of swiftFiles) {
222
+ const fileRefUuid = project.generateUuid();
223
+ objects.PBXFileReference[fileRefUuid] = {
224
+ isa: "PBXFileReference",
225
+ lastKnownFileType: "sourcecode.swift",
226
+ path: fileName,
227
+ sourceTree: "\"<group>\"",
228
+ name: fileName
229
+ };
230
+ objects.PBXFileReference[`${fileRefUuid}_comment`] = fileName;
231
+ sourceFileRefs.push({
232
+ uuid: fileRefUuid,
233
+ name: fileName
234
+ });
235
+ }
236
+ const infoPlistRefUuid = project.generateUuid();
237
+ objects.PBXFileReference[infoPlistRefUuid] = {
238
+ isa: "PBXFileReference",
239
+ lastKnownFileType: "text.plist.xml",
240
+ path: "Info.plist",
241
+ sourceTree: "\"<group>\"",
242
+ name: "Info.plist"
243
+ };
244
+ objects.PBXFileReference[`${infoPlistRefUuid}_comment`] = "Info.plist";
245
+ const groupUuid = project.generateUuid();
246
+ const groupChildren = [...sourceFileRefs.map((ref) => ({
247
+ value: ref.uuid,
248
+ comment: ref.name
249
+ })), {
250
+ value: infoPlistRefUuid,
251
+ comment: "Info.plist"
252
+ }];
253
+ objects.PBXGroup[groupUuid] = {
254
+ isa: "PBXGroup",
255
+ children: groupChildren,
256
+ name: serviceName,
257
+ path: serviceName,
258
+ sourceTree: "\"<group>\""
259
+ };
260
+ objects.PBXGroup[`${groupUuid}_comment`] = serviceName;
261
+ const firstProject = project.getFirstProject();
262
+ const mainGroupKey = firstProject.firstProject.mainGroup;
263
+ const mainGroup = project.getPBXGroupByKey(mainGroupKey);
264
+ if (mainGroup) mainGroup.children.push({
265
+ value: groupUuid,
266
+ comment: serviceName
267
+ });
268
+ const buildFileUuids = [];
269
+ for (const ref of sourceFileRefs) {
270
+ const buildFileUuid = project.generateUuid();
271
+ objects.PBXBuildFile[buildFileUuid] = {
272
+ isa: "PBXBuildFile",
273
+ fileRef: ref.uuid,
274
+ fileRef_comment: ref.name
275
+ };
276
+ objects.PBXBuildFile[`${buildFileUuid}_comment`] = `${ref.name} in Sources`;
277
+ buildFileUuids.push({
278
+ uuid: buildFileUuid,
279
+ name: ref.name
280
+ });
281
+ }
282
+ const sourcesBuildPhaseUuid = project.generateUuid();
283
+ objects.PBXSourcesBuildPhase = objects.PBXSourcesBuildPhase || {};
284
+ objects.PBXSourcesBuildPhase[sourcesBuildPhaseUuid] = {
285
+ isa: "PBXSourcesBuildPhase",
286
+ buildActionMask: 2147483647,
287
+ files: buildFileUuids.map((bf) => ({
288
+ value: bf.uuid,
289
+ comment: `${bf.name} in Sources`
290
+ })),
291
+ runOnlyForDeploymentPostprocessing: 0
292
+ };
293
+ objects.PBXSourcesBuildPhase[`${sourcesBuildPhaseUuid}_comment`] = "Sources";
294
+ const spmBuildFileUuid = project.generateUuid();
295
+ objects.PBXBuildFile[spmBuildFileUuid] = {
296
+ isa: "PBXBuildFile",
297
+ productRef: spmProductDepUuid,
298
+ productRef_comment: SPM_PRODUCT_NAME
299
+ };
300
+ objects.PBXBuildFile[`${spmBuildFileUuid}_comment`] = `${SPM_PRODUCT_NAME} in Frameworks`;
301
+ const frameworkFiles = [{
302
+ value: spmBuildFileUuid,
303
+ comment: `${SPM_PRODUCT_NAME} in Frameworks`
304
+ }];
305
+ for (const remoteDep of remoteProductDeps) {
306
+ const remoteBuildFileUuid = project.generateUuid();
307
+ objects.PBXBuildFile[remoteBuildFileUuid] = {
308
+ isa: "PBXBuildFile",
309
+ productRef: remoteDep.productDepUuid,
310
+ productRef_comment: remoteDep.productName
311
+ };
312
+ objects.PBXBuildFile[`${remoteBuildFileUuid}_comment`] = `${remoteDep.productName} in Frameworks`;
313
+ frameworkFiles.push({
314
+ value: remoteBuildFileUuid,
315
+ comment: `${remoteDep.productName} in Frameworks`
316
+ });
317
+ }
318
+ const frameworksBuildPhaseUuid = project.generateUuid();
319
+ objects.PBXFrameworksBuildPhase = objects.PBXFrameworksBuildPhase || {};
320
+ objects.PBXFrameworksBuildPhase[frameworksBuildPhaseUuid] = {
321
+ isa: "PBXFrameworksBuildPhase",
322
+ buildActionMask: 2147483647,
323
+ files: frameworkFiles,
324
+ runOnlyForDeploymentPostprocessing: 0
325
+ };
326
+ objects.PBXFrameworksBuildPhase[`${frameworksBuildPhaseUuid}_comment`] = "Frameworks";
327
+ const targetUuid = project.generateUuid();
328
+ const nativeTarget = {
329
+ isa: "PBXNativeTarget",
330
+ name: serviceName,
331
+ productName: serviceName,
332
+ productReference: productFileRefUuid,
333
+ productType: `"${XPC_PRODUCT_TYPE}"`,
334
+ buildConfigurationList: configList.uuid,
335
+ buildPhases: [{
336
+ value: sourcesBuildPhaseUuid,
337
+ comment: "Sources"
338
+ }, {
339
+ value: frameworksBuildPhaseUuid,
340
+ comment: "Frameworks"
341
+ }],
342
+ buildRules: [],
343
+ dependencies: []
344
+ };
345
+ nativeTarget.packageProductDependencies = [{
346
+ value: spmProductDepUuid,
347
+ comment: SPM_PRODUCT_NAME
348
+ }, ...remoteProductDeps.map((dep) => ({
349
+ value: dep.productDepUuid,
350
+ comment: dep.productName
351
+ }))];
352
+ objects.PBXNativeTarget[targetUuid] = nativeTarget;
353
+ objects.PBXNativeTarget[`${targetUuid}_comment`] = serviceName;
354
+ for (const key of Object.keys(projectSection)) if (!key.endsWith("_comment")) projectSection[key].targets.push({
355
+ value: targetUuid,
356
+ comment: serviceName
357
+ });
358
+ const appTarget = project.getFirstTarget();
359
+ const appTargetUuid = appTarget.uuid;
360
+ const xpcCopyBuildFileUuid = project.generateUuid();
361
+ objects.PBXBuildFile[xpcCopyBuildFileUuid] = {
362
+ isa: "PBXBuildFile",
363
+ fileRef: productFileRefUuid,
364
+ fileRef_comment: `${serviceName}.xpc`,
365
+ settings: { ATTRIBUTES: ["RemoveHeadersOnCopy"] }
366
+ };
367
+ objects.PBXBuildFile[`${xpcCopyBuildFileUuid}_comment`] = `${serviceName}.xpc in Embed XPC Services`;
368
+ const copyPhaseUuid = project.generateUuid();
369
+ const copyPhaseSection = objects["PBXCopyFilesBuildPhase"] || (objects["PBXCopyFilesBuildPhase"] = {});
370
+ copyPhaseSection[copyPhaseUuid] = {
371
+ isa: "PBXCopyFilesBuildPhase",
372
+ buildActionMask: 2147483647,
373
+ dstPath: "\"$(CONTENTS_FOLDER_PATH)/XPCServices\"",
374
+ dstSubfolderSpec: 16,
375
+ files: [{
376
+ value: xpcCopyBuildFileUuid,
377
+ comment: `${serviceName}.xpc in Embed XPC Services`
378
+ }],
379
+ name: "\"Embed XPC Services\"",
380
+ runOnlyForDeploymentPostprocessing: 0
381
+ };
382
+ copyPhaseSection[`${copyPhaseUuid}_comment`] = "Embed XPC Services";
383
+ const appNativeTarget = objects.PBXNativeTarget[appTargetUuid] || appTarget.firstTarget;
384
+ if (appNativeTarget && "buildPhases" in appNativeTarget) appNativeTarget.buildPhases.push({
385
+ value: copyPhaseUuid,
386
+ comment: "Embed XPC Services"
387
+ });
388
+ const proxyUuid = project.generateUuid();
389
+ const proxySection = objects["PBXContainerItemProxy"] || (objects["PBXContainerItemProxy"] = {});
390
+ proxySection[proxyUuid] = {
391
+ isa: "PBXContainerItemProxy",
392
+ containerPortal: project.hash.project.rootObject,
393
+ containerPortal_comment: "Project object",
394
+ proxyType: 1,
395
+ remoteGlobalIDString: targetUuid,
396
+ remoteInfo: serviceName
397
+ };
398
+ proxySection[`${proxyUuid}_comment`] = "PBXContainerItemProxy";
399
+ const depUuid = project.generateUuid();
400
+ const depSection = objects["PBXTargetDependency"] || (objects["PBXTargetDependency"] = {});
401
+ depSection[depUuid] = {
402
+ isa: "PBXTargetDependency",
403
+ target: targetUuid,
404
+ target_comment: serviceName,
405
+ targetProxy: proxyUuid,
406
+ targetProxy_comment: "PBXContainerItemProxy"
407
+ };
408
+ depSection[`${depUuid}_comment`] = "PBXTargetDependency";
409
+ if (appNativeTarget && "dependencies" in appNativeTarget) appNativeTarget.dependencies.push({
410
+ value: depUuid,
411
+ comment: "PBXTargetDependency"
412
+ });
413
+ const projectObj = firstProject.firstProject;
414
+ if (!projectObj.attributes.TargetAttributes) projectObj.attributes.TargetAttributes = {};
415
+ projectObj.attributes.TargetAttributes[targetUuid] = {
416
+ CreatedOnToolsVersion: "15.0",
417
+ DevelopmentTeam: teamId
418
+ };
419
+ console.log(`[xpc] Added XPC target "${serviceName}" (${bundleId}) with SPM dependency`);
420
+ return config;
421
+ });
422
+ return config;
423
+ };
424
+ const pkg = {
425
+ name: "@macify/xpc",
426
+ version: "0.0.1"
427
+ };
428
+ var src_default = (0, _expo_config_plugins.createRunOncePlugin)(withXPCService, pkg.name, pkg.version);
429
+
430
+ //#endregion
431
+ module.exports = src_default;
@@ -0,0 +1,17 @@
1
+ import { ConfigPlugin } from "@expo/config-plugins";
2
+
3
+ //#region plugin/src/index.d.ts
4
+ type RemoteSPMDependency = {
5
+ url: string;
6
+ version?: string;
7
+ exactVersion?: string;
8
+ branch?: string;
9
+ products: string[];
10
+ };
11
+ type PluginProps = {
12
+ serviceName?: string;
13
+ macosDeploymentTarget?: string;
14
+ dependencies?: RemoteSPMDependency[];
15
+ };
16
+ declare const _default: ConfigPlugin<PluginProps>;
17
+ export = _default;
@@ -0,0 +1,19 @@
1
+ // swift-tools-version: 5.9
2
+ import PackageDescription
3
+
4
+ let package = Package(
5
+ name: "MacifyXPCService",
6
+ platforms: [.macOS(.v13)],
7
+ products: [
8
+ .library(
9
+ name: "MacifyXPCService",
10
+ targets: ["MacifyXPCService"]
11
+ ),
12
+ ],
13
+ targets: [
14
+ .target(
15
+ name: "MacifyXPCService",
16
+ path: "Sources/MacifyXPCService"
17
+ ),
18
+ ]
19
+ )
@@ -0,0 +1,51 @@
1
+ import Foundation
2
+
3
+ /// The XPC service implementation that receives generic JSON RPC calls
4
+ /// and dispatches them to the user's `XPCMessageHandler`.
5
+ public class MacifyServiceImpl: NSObject, MacifyXPCProtocol {
6
+ private let handler: XPCMessageHandler
7
+ private let emitter: XPCEventEmitter
8
+
9
+ public init(handler: XPCMessageHandler, connection: NSXPCConnection) {
10
+ self.handler = handler
11
+ self.emitter = XPCEventEmitter(connection: connection)
12
+ super.init()
13
+ }
14
+
15
+ public func call(
16
+ type: String,
17
+ payload: Data,
18
+ withReply reply: @escaping (Data?, NSError?) -> Void
19
+ ) {
20
+ Task {
21
+ do {
22
+ // Decode incoming JSON payload
23
+ let decoded: [String: Any]
24
+ if payload.isEmpty {
25
+ decoded = [:]
26
+ } else {
27
+ guard let json = try JSONSerialization.jsonObject(
28
+ with: payload, options: []
29
+ ) as? [String: Any] else {
30
+ throw MacifyXPCError.invalidPayload
31
+ }
32
+ decoded = json
33
+ }
34
+
35
+ // Dispatch to user's handler
36
+ let result = try await handler.handle(
37
+ type: type, payload: decoded, emitter: emitter
38
+ )
39
+
40
+ // Encode response
41
+ let responseData = try JSONSerialization.data(
42
+ withJSONObject: result, options: []
43
+ )
44
+ reply(responseData, nil)
45
+ } catch {
46
+ let nsError = error as NSError
47
+ reply(nil, nsError)
48
+ }
49
+ }
50
+ }
51
+ }
@@ -0,0 +1,22 @@
1
+ import Foundation
2
+
3
+ /// Errors that can occur in the XPC service.
4
+ public enum MacifyXPCError: LocalizedError {
5
+ case unknownMethod(String)
6
+ case invalidPayload
7
+ case serializationFailed(String)
8
+ case connectionLost
9
+
10
+ public var errorDescription: String? {
11
+ switch self {
12
+ case .unknownMethod(let method):
13
+ return "Unknown XPC method: \(method)"
14
+ case .invalidPayload:
15
+ return "Invalid JSON payload"
16
+ case .serializationFailed(let detail):
17
+ return "Serialization failed: \(detail)"
18
+ case .connectionLost:
19
+ return "XPC connection was lost"
20
+ }
21
+ }
22
+ }
@@ -0,0 +1,21 @@
1
+ import Foundation
2
+
3
+ /// Wire protocol for RPC calls from the app to the XPC service.
4
+ /// Both the app-side pod and the XPC service SPM library compile their own
5
+ /// copy of this protocol definition.
6
+ @objc public protocol MacifyXPCProtocol {
7
+ /// Generic RPC: the app sends a message type and JSON-encoded payload,
8
+ /// the XPC service processes it and replies with JSON-encoded data or an error.
9
+ func call(
10
+ type: String,
11
+ payload: Data,
12
+ withReply reply: @escaping (Data?, NSError?) -> Void
13
+ )
14
+ }
15
+
16
+ /// Reverse channel: the XPC service pushes events back to the app.
17
+ /// The app sets its exported object conforming to this protocol on the
18
+ /// NSXPCConnection so the XPC service can call back into it.
19
+ @objc public protocol MacifyXPCEventReceiver {
20
+ func handleEvent(type: String, payload: Data)
21
+ }
@@ -0,0 +1,29 @@
1
+ import Foundation
2
+
3
+ /// Allows the XPC service to push events back to the app.
4
+ /// The emitter holds a weak reference to the connection's event receiver proxy.
5
+ public class XPCEventEmitter {
6
+ private weak var connection: NSXPCConnection?
7
+
8
+ init(connection: NSXPCConnection) {
9
+ self.connection = connection
10
+ }
11
+
12
+ /// Send an event to the app.
13
+ /// - Parameters:
14
+ /// - type: Event type string (e.g. "progress", "status")
15
+ /// - payload: Dictionary that will be JSON-encoded
16
+ public func emit(_ type: String, payload: [String: Any] = [:]) {
17
+ guard let connection = connection else { return }
18
+
19
+ do {
20
+ let data = try JSONSerialization.data(
21
+ withJSONObject: payload, options: []
22
+ )
23
+ let proxy = connection.remoteObjectProxy as? MacifyXPCEventReceiver
24
+ proxy?.handleEvent(type: type, payload: data)
25
+ } catch {
26
+ print("[MacifyXPCService] Failed to serialize event payload: \(error)")
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,18 @@
1
+ import Foundation
2
+
3
+ /// Protocol that users implement to handle XPC messages.
4
+ /// The user's `main.swift` creates a struct conforming to this and passes
5
+ /// it to `XPCServiceHost.start(handler:)`.
6
+ public protocol XPCMessageHandler {
7
+ /// Handle an incoming RPC call.
8
+ /// - Parameters:
9
+ /// - type: The message type string (e.g. "ping", "processImage")
10
+ /// - payload: Decoded JSON payload as a dictionary
11
+ /// - emitter: Used to push events back to the app
12
+ /// - Returns: A dictionary that will be JSON-encoded and sent back as the reply
13
+ func handle(
14
+ type: String,
15
+ payload: [String: Any],
16
+ emitter: XPCEventEmitter
17
+ ) async throws -> [String: Any]
18
+ }
@@ -0,0 +1,75 @@
1
+ import Foundation
2
+
3
+ /// Entry point for XPC services built with MacifyXPCService.
4
+ ///
5
+ /// Usage in `main.swift`:
6
+ /// ```swift
7
+ /// import MacifyXPCService
8
+ ///
9
+ /// struct MyHandler: XPCMessageHandler {
10
+ /// func handle(type: String, payload: [String: Any], emitter: XPCEventEmitter) async throws -> [String: Any] {
11
+ /// switch type {
12
+ /// case "ping":
13
+ /// return ["message": "pong"]
14
+ /// default:
15
+ /// throw MacifyXPCError.unknownMethod(type)
16
+ /// }
17
+ /// }
18
+ /// }
19
+ ///
20
+ /// XPCServiceHost.start(handler: MyHandler())
21
+ /// ```
22
+ public enum XPCServiceHost {
23
+ /// Start the XPC service listener with the given message handler.
24
+ /// This function never returns — it runs the main run loop.
25
+ public static func start(handler: XPCMessageHandler) {
26
+ let delegate = ServiceDelegate(handler: handler)
27
+ let listener = NSXPCListener.service()
28
+ listener.delegate = delegate
29
+
30
+ // Keep a strong reference so the delegate isn't deallocated
31
+ _delegate = delegate
32
+
33
+ listener.resume()
34
+ }
35
+
36
+ // Strong reference to prevent deallocation
37
+ private static var _delegate: ServiceDelegate?
38
+ }
39
+
40
+ /// Internal delegate that vends `MacifyServiceImpl` instances to incoming connections.
41
+ class ServiceDelegate: NSObject, NSXPCListenerDelegate {
42
+ let handler: XPCMessageHandler
43
+
44
+ init(handler: XPCMessageHandler) {
45
+ self.handler = handler
46
+ super.init()
47
+ }
48
+
49
+ func listener(
50
+ _ listener: NSXPCListener,
51
+ shouldAcceptNewConnection connection: NSXPCConnection
52
+ ) -> Bool {
53
+ // Set the interface the service exports (app calls us)
54
+ connection.exportedInterface = NSXPCInterface(
55
+ with: MacifyXPCProtocol.self
56
+ )
57
+
58
+ // Set the interface the app exports (we call back for events)
59
+ connection.remoteObjectInterface = NSXPCInterface(
60
+ with: MacifyXPCEventReceiver.self
61
+ )
62
+
63
+ // Vend the service implementation
64
+ connection.exportedObject = MacifyServiceImpl(
65
+ handler: handler, connection: connection
66
+ )
67
+
68
+ connection.invalidationHandler = {
69
+ // Connection cleaned up
70
+ }
71
+
72
+ connection.resume()
73
+ return true
74
+ }
75
+ }