@therealitycrafters/react-native-evskit 0.1.0 → 0.1.1

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,7 @@
1
+ const withEverysightBundleResources = require('./plugin/withEverysightBundleResources');
2
+
3
+ module.exports = (config = {}, { bundleResources = [] } = {}) => {
4
+ let result = config;
5
+ result = withEverysightBundleResources(result, { bundleResources });
6
+ return result;
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@therealitycrafters/react-native-evskit",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "React Native / Expo bridge for Everysight's Maverick SDK (EvsKit + NativeEvsKit)",
5
5
  "author": "Patrick Catanzariti",
6
6
  "homepage": "https://therealitycrafters.com",
@@ -17,6 +17,8 @@
17
17
  "src",
18
18
  "android",
19
19
  "ios",
20
+ "plugin",
21
+ "app.plugin.js",
20
22
  "expo-module.config.json"
21
23
  ],
22
24
  "scripts": {
@@ -0,0 +1,110 @@
1
+ const { withXcodeProject } = require("@expo/config-plugins");
2
+ const path = require("path");
3
+
4
+ /**
5
+ * Config plugin that adds files (e.g. sdk.key, app.key) as Xcode bundle
6
+ * resources so they are included in the app binary.
7
+ *
8
+ * Usage in app.config.ts:
9
+ * plugins: [
10
+ * ['react-native-evskit', { bundleResources: ['./sdk.key'] }],
11
+ * ]
12
+ *
13
+ * Adds PBXBuildFile + PBXFileReference entries directly to the pbxproj
14
+ * objects, avoiding the pbxFile constructor which produces undefined values
15
+ * that CocoaPods cannot parse.
16
+ */
17
+ function withEverysightBundleResources(config, { bundleResources = [] } = {}) {
18
+ if (!bundleResources.length) {
19
+ return config;
20
+ }
21
+
22
+ return withXcodeProject(config, (config) => {
23
+ const project = config.modResults;
24
+ const objects = project.hash.project.objects;
25
+
26
+ // Initialize required object sections
27
+ objects.PBXBuildFile = objects.PBXBuildFile || {};
28
+ objects.PBXFileReference = objects.PBXFileReference || {};
29
+
30
+ for (const relativePath of bundleResources) {
31
+ const resolvedPath = path.join(
32
+ config.modRequest.projectRoot,
33
+ relativePath,
34
+ );
35
+ const fileName = path.basename(resolvedPath);
36
+
37
+ // Skip if already exists
38
+ const existingRef = Object.keys(objects.PBXFileReference).find(
39
+ (key) =>
40
+ !key.endsWith("_comment") &&
41
+ objects.PBXFileReference[key].name === fileName,
42
+ );
43
+ if (existingRef) continue;
44
+
45
+ // The Xcode project lives in <projectRoot>/ios, so the file reference
46
+ // path must be relative to that directory (e.g. "../sdk.key").
47
+ const iosDir = path.join(config.modRequest.projectRoot, "ios");
48
+ const relativeToIos = path.relative(iosDir, resolvedPath);
49
+
50
+ // Create PBXFileReference
51
+ const fileRefUuid = project.generateUuid();
52
+ objects.PBXFileReference[fileRefUuid] = {
53
+ isa: "PBXFileReference",
54
+ name: `"${fileName}"`,
55
+ path: `"${relativeToIos}"`,
56
+ // Quotes must be embedded in the string: the `xcode` lib's pbxWriter
57
+ // does not add quotes itself, and angle brackets (<>) are invalid
58
+ // unquoted in the pbxproj format (e.g. `sourceTree = "<group>";`).
59
+ sourceTree: '"<group>"',
60
+ };
61
+ objects.PBXFileReference[`${fileRefUuid}_comment`] = fileName;
62
+
63
+ // Create PBXBuildFile
64
+ const buildFileUuid = project.generateUuid();
65
+ objects.PBXBuildFile[buildFileUuid] = {
66
+ isa: "PBXBuildFile",
67
+ fileRef: fileRefUuid,
68
+ };
69
+ objects.PBXBuildFile[`${buildFileUuid}_comment`] =
70
+ `${fileName} in Resources`;
71
+
72
+ // Add to Resources build phase
73
+ const target = project.getFirstTarget();
74
+ const nativeTarget = objects.PBXNativeTarget[target.uuid];
75
+ const resourcesPhaseUuid = nativeTarget.buildPhases.find((phase) =>
76
+ /Resources/i.test(phase.comment || ""),
77
+ )?.value;
78
+ if (resourcesPhaseUuid && objects.PBXResourcesBuildPhase) {
79
+ const resourcesPhase =
80
+ objects.PBXResourcesBuildPhase[resourcesPhaseUuid];
81
+ if (resourcesPhase) {
82
+ resourcesPhase.files = resourcesPhase.files || [];
83
+ resourcesPhase.files.push({
84
+ value: buildFileUuid,
85
+ comment: `${fileName} in Resources`,
86
+ });
87
+ }
88
+ }
89
+
90
+ // Add to Resources group
91
+ const resourcesGroupKey = Object.keys(objects.PBXGroup || {}).find(
92
+ (key) =>
93
+ !key.endsWith("_comment") &&
94
+ objects.PBXGroup[key].name === "Resources",
95
+ );
96
+ if (resourcesGroupKey) {
97
+ objects.PBXGroup[resourcesGroupKey].children =
98
+ objects.PBXGroup[resourcesGroupKey].children || [];
99
+ objects.PBXGroup[resourcesGroupKey].children.push({
100
+ value: fileRefUuid,
101
+ comment: fileName,
102
+ });
103
+ }
104
+ }
105
+
106
+ return config;
107
+ });
108
+ }
109
+
110
+ module.exports = withEverysightBundleResources;
@@ -0,0 +1,276 @@
1
+ const { withXcodeProject } = require("@expo/config-plugins");
2
+ const fs = require("fs");
3
+ const path = require("path");
4
+
5
+ /**
6
+ * Adds Everysight's Maverick SDK (EvsKit + NativeEvsKit) as a Swift Package
7
+ * Manager dependency of the iOS app target during `expo prebuild`.
8
+ *
9
+ * Why this exists: CocoaPods can't consume SPM packages, and the SDK is
10
+ * only distributed via SPM (github.com/everysight-maverick/m1-ios-spm),
11
+ * which itself is just a `Package.swift` pointing at two prebuilt
12
+ * `EvsKit.xcframework.zip` / `NativeEvsKit.xcframework.zip` binary targets
13
+ * (confirmed by reading that repo directly — there's no source in it).
14
+ * Without this plugin you'd have to re-add the package by hand in Xcode
15
+ * every time you run a clean prebuild, which defeats the point of managed
16
+ * config.
17
+ *
18
+ * Implementation note: `@expo/config-plugins` / the underlying `xcode` npm
19
+ * package has no first-class API for SPM package references (it predates
20
+ * SPM support landing in most versions of that library), so this writes the
21
+ * `XCRemoteSwiftPackageReference` / `XCSwiftPackageProductDependency`
22
+ * pbxproj objects directly. That's inherently more fragile than a proper
23
+ * API — it's been structured to be idempotent (safe to run on every
24
+ * prebuild) and defensive about missing sections, but if a future Xcode
25
+ * project format changes the relevant object shapes, this may need
26
+ * updating. If it ever misbehaves, the fallback is adding the package by
27
+ * hand via Xcode's File > Add Packages... using the same URL/version below,
28
+ * then removing this plugin.
29
+ *
30
+ * @param {import('@expo/config-plugins').ExportedConfig} config
31
+ * @param {{ version?: string }} [options]
32
+ */
33
+ function withEverysightSPM(config, options = {}) {
34
+ const packageURL = "https://github.com/everysight-maverick/m1-ios-spm.git";
35
+ const versionRequirement = options.version ?? "2.6.1";
36
+ const products = ["EvsKit", "NativeEvsKit"];
37
+
38
+ config = withXcodeProject(config, (config) => {
39
+ const project = config.modResults;
40
+ addSwiftPackage(project, packageURL, versionRequirement, products);
41
+ addModuleSourcesToAppTarget(project, config.modRequest.projectRoot);
42
+ return config;
43
+ });
44
+
45
+ return config;
46
+ }
47
+
48
+ /**
49
+ * Registers a remote Swift package (with an exact-version requirement) and
50
+ * links the given product names against the app's first native target.
51
+ * Safe to call repeatedly — checks for an existing reference/dependency
52
+ * before adding a duplicate.
53
+ */
54
+ function addSwiftPackage(project, repositoryURL, exactVersion, productNames) {
55
+ const objects = project.hash.project.objects;
56
+ objects.XCRemoteSwiftPackageReference =
57
+ objects.XCRemoteSwiftPackageReference || {};
58
+ objects.XCSwiftPackageProductDependency =
59
+ objects.XCSwiftPackageProductDependency || {};
60
+
61
+ // --- 1. Find or create the XCRemoteSwiftPackageReference ------------------
62
+ let packageRefUuid = Object.keys(objects.XCRemoteSwiftPackageReference).find(
63
+ (key) =>
64
+ !key.endsWith("_comment") &&
65
+ objects.XCRemoteSwiftPackageReference[key].repositoryURL ===
66
+ JSON.stringify(repositoryURL),
67
+ );
68
+
69
+ if (!packageRefUuid) {
70
+ packageRefUuid = project.generateUuid();
71
+ objects.XCRemoteSwiftPackageReference[packageRefUuid] = {
72
+ isa: "XCRemoteSwiftPackageReference",
73
+ repositoryURL: JSON.stringify(repositoryURL),
74
+ requirement: {
75
+ kind: "exactVersion",
76
+ version: exactVersion,
77
+ },
78
+ };
79
+ objects.XCRemoteSwiftPackageReference[`${packageRefUuid}_comment`] =
80
+ 'XCRemoteSwiftPackageReference "m1-ios-spm"';
81
+
82
+ // Register the reference on the project object itself
83
+ // (Xcode reads packageReferences off PBXProject, not just the target).
84
+ const rootObjectUuid = project.hash.project.rootObject;
85
+ const rootObject = objects.PBXProject[rootObjectUuid];
86
+ rootObject.packageReferences = rootObject.packageReferences || [];
87
+ if (
88
+ !rootObject.packageReferences.some((ref) => ref.value === packageRefUuid)
89
+ ) {
90
+ rootObject.packageReferences.push({
91
+ value: packageRefUuid,
92
+ comment: 'XCRemoteSwiftPackageReference "m1-ios-spm"',
93
+ });
94
+ }
95
+ }
96
+
97
+ // --- 2. Find or create a XCSwiftPackageProductDependency per product ------
98
+ const target = project.getFirstTarget();
99
+ const nativeTarget = objects.PBXNativeTarget[target.uuid];
100
+ nativeTarget.packageProductDependencies =
101
+ nativeTarget.packageProductDependencies || [];
102
+
103
+ // Also link SPM products to the pod target (RNEvsKit) so that the Swift
104
+ // module can import EvsKit and NativeEvsKit. The pod is compiled as a
105
+ // separate target and needs the SPM products linked to it.
106
+ const podTargetUuid = Object.keys(objects.PBXNativeTarget).find(
107
+ (key) =>
108
+ !key.endsWith("_comment") &&
109
+ objects.PBXNativeTarget[key].productReference &&
110
+ objects.PBXNativeTarget[key].buildConfigurationList,
111
+ );
112
+ const podTarget = podTargetUuid
113
+ ? objects.PBXNativeTarget[podTargetUuid]
114
+ : null;
115
+
116
+ const frameworksBuildPhaseUuid = nativeTarget.buildPhases.find((phase) =>
117
+ /Frameworks/i.test(phase.comment || ""),
118
+ )?.value;
119
+ const frameworksPhase = frameworksBuildPhaseUuid
120
+ ? objects.PBXFrameworksBuildPhase[frameworksBuildPhaseUuid]
121
+ : undefined;
122
+
123
+ for (const productName of productNames) {
124
+ let productDepUuid = Object.keys(
125
+ objects.XCSwiftPackageProductDependency,
126
+ ).find(
127
+ (key) =>
128
+ !key.endsWith("_comment") &&
129
+ objects.XCSwiftPackageProductDependency[key].productName ===
130
+ productName,
131
+ );
132
+
133
+ if (!productDepUuid) {
134
+ productDepUuid = project.generateUuid();
135
+ objects.XCSwiftPackageProductDependency[productDepUuid] = {
136
+ isa: "XCSwiftPackageProductDependency",
137
+ package: packageRefUuid,
138
+ productName,
139
+ };
140
+ objects.XCSwiftPackageProductDependency[`${productDepUuid}_comment`] =
141
+ productName;
142
+ }
143
+
144
+ // Add to app target
145
+ if (
146
+ !nativeTarget.packageProductDependencies.some(
147
+ (dep) => dep.value === productDepUuid,
148
+ )
149
+ ) {
150
+ nativeTarget.packageProductDependencies.push({
151
+ value: productDepUuid,
152
+ comment: productName,
153
+ });
154
+ }
155
+
156
+ // Also add to pod target (RNEvsKit) so the Swift module can import these
157
+ if (podTarget) {
158
+ podTarget.packageProductDependencies =
159
+ podTarget.packageProductDependencies || [];
160
+ if (
161
+ !podTarget.packageProductDependencies.some(
162
+ (dep) => dep.value === productDepUuid,
163
+ )
164
+ ) {
165
+ podTarget.packageProductDependencies.push({
166
+ value: productDepUuid,
167
+ comment: productName,
168
+ });
169
+ }
170
+ }
171
+
172
+ // Link the product into the app binary via the Frameworks build phase,
173
+ // same as Xcode would if you added it through the UI.
174
+ if (frameworksPhase) {
175
+ objects.PBXBuildFile = objects.PBXBuildFile || {};
176
+ const alreadyLinked = Object.keys(objects.PBXBuildFile).some(
177
+ (key) =>
178
+ !key.endsWith("_comment") &&
179
+ objects.PBXBuildFile[key].productRef === productDepUuid,
180
+ );
181
+
182
+ if (!alreadyLinked) {
183
+ const buildFileUuid = project.generateUuid();
184
+ objects.PBXBuildFile[buildFileUuid] = {
185
+ isa: "PBXBuildFile",
186
+ productRef: productDepUuid,
187
+ productRef_comment: productName,
188
+ };
189
+ objects.PBXBuildFile[`${buildFileUuid}_comment`] =
190
+ `${productName} in Frameworks`;
191
+
192
+ frameworksPhase.files = frameworksPhase.files || [];
193
+ frameworksPhase.files.push({
194
+ value: buildFileUuid,
195
+ comment: `${productName} in Frameworks`,
196
+ });
197
+ }
198
+ }
199
+ }
200
+ }
201
+
202
+ /**
203
+ * Adds the EvsKitModule.swift source files directly to the app target's
204
+ * Sources build phase so they can import SPM products (EvsKit, NativeEvsKit).
205
+ * CocoaPods targets cannot consume SPM packages, so the podspec has empty
206
+ * source_files and the actual sources are compiled via the app target instead.
207
+ */
208
+ function addModuleSourcesToAppTarget(project, projectRoot) {
209
+ const objects = project.hash.project.objects;
210
+ const target = project.getFirstTarget();
211
+ const nativeTarget = objects.PBXNativeTarget[target.uuid];
212
+
213
+ // Find the Sources build phase
214
+ const sourcesPhaseUuid = nativeTarget.buildPhases.find((phase) =>
215
+ /Sources/i.test(phase.comment || ""),
216
+ )?.value;
217
+ const sourcesPhase = sourcesPhaseUuid
218
+ ? objects.PBXSourcesBuildPhase[sourcesPhaseUuid]
219
+ : undefined;
220
+ if (!sourcesPhase) return;
221
+
222
+ const moduleDir = path.join(path.dirname(__dirname), "ios");
223
+ const sourceFiles = fs
224
+ .readdirSync(moduleDir)
225
+ .filter((f) => f.endsWith(".swift"));
226
+
227
+ // The Xcode project lives in <projectRoot>/ios, so the file reference path
228
+ // must be relative to that directory (e.g. "../../react-native-evskit/ios").
229
+ const iosDir = path.join(projectRoot, "ios");
230
+ const moduleDirRelativeToIos = path.relative(iosDir, moduleDir);
231
+
232
+ for (const fileName of sourceFiles) {
233
+ // Check if already added
234
+ const alreadyAdded = Object.keys(objects.PBXBuildFile || {}).some(
235
+ (key) =>
236
+ !key.endsWith("_comment") &&
237
+ objects.PBXBuildFile[key].fileRef &&
238
+ objects.PBXBuildFile[key].fileRef_comment === fileName,
239
+ );
240
+ if (alreadyAdded) continue;
241
+
242
+ // Add file reference
243
+ const fileRefUuid = project.generateUuid();
244
+ objects.PBXFileReference = objects.PBXFileReference || {};
245
+ objects.PBXFileReference[fileRefUuid] = {
246
+ isa: "PBXFileReference",
247
+ explicitFileType: "sourcecode.swift",
248
+ fileEncoding: 4,
249
+ name: fileName,
250
+ path: path.join(moduleDirRelativeToIos, fileName),
251
+ // Quotes must be embedded in the string: the `xcode` lib's pbxWriter
252
+ // does not add quotes itself, and angle brackets (<>) are invalid
253
+ // unquoted in the pbxproj format (e.g. `sourceTree = "<group>";`).
254
+ sourceTree: '"<group>"',
255
+ };
256
+ objects.PBXFileReference[`${fileRefUuid}_comment`] = fileName;
257
+
258
+ // Add build file
259
+ const buildFileUuid = project.generateUuid();
260
+ objects.PBXBuildFile = objects.PBXBuildFile || {};
261
+ objects.PBXBuildFile[buildFileUuid] = {
262
+ isa: "PBXBuildFile",
263
+ fileRef: fileRefUuid,
264
+ };
265
+ objects.PBXBuildFile[`${buildFileUuid}_comment`] = `${fileName} in Sources`;
266
+
267
+ // Add to Sources build phase
268
+ sourcesPhase.files = sourcesPhase.files || [];
269
+ sourcesPhase.files.push({
270
+ value: buildFileUuid,
271
+ comment: `${fileName} in Sources`,
272
+ });
273
+ }
274
+ }
275
+
276
+ module.exports = withEverysightSPM;