rork-xcode 0.1.0 → 0.2.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.
- package/README.md +155 -2
- package/dist/index.d.ts +1121 -1
- package/dist/index.js +2047 -23
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -14,7 +14,7 @@ function isDictionary(value) {
|
|
|
14
14
|
* Field access on parsed documents goes through this because any field of
|
|
15
15
|
* an untrusted document can hold any value type.
|
|
16
16
|
*/
|
|
17
|
-
function asString(value) {
|
|
17
|
+
function asString$1(value) {
|
|
18
18
|
return typeof value === "string" ? value : void 0;
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
@@ -70,33 +70,33 @@ function createReferenceComments(root) {
|
|
|
70
70
|
for (const ownerId of Object.keys(objects)) {
|
|
71
71
|
const owner = objects[ownerId];
|
|
72
72
|
if (!isDictionary(owner)) continue;
|
|
73
|
-
const isa = asString(owner["isa"]) ?? "";
|
|
73
|
+
const isa = asString$1(owner["isa"]) ?? "";
|
|
74
74
|
if (isa.endsWith("BuildPhase")) indexFirst(fileToPhase, owner["files"], {
|
|
75
75
|
isa,
|
|
76
|
-
name: asString(owner["name"])
|
|
76
|
+
name: asString$1(owner["name"])
|
|
77
77
|
});
|
|
78
78
|
else if (isa === "PBXContainerItemProxy") {
|
|
79
|
-
const portal = asString(owner["containerPortal"]);
|
|
80
|
-
const remoteInfo = asString(owner["remoteInfo"]);
|
|
79
|
+
const portal = asString$1(owner["containerPortal"]);
|
|
80
|
+
const remoteInfo = asString$1(owner["remoteInfo"]);
|
|
81
81
|
if (portal != null && remoteInfo != null && !proxyRemoteInfoByPortal.has(portal)) proxyRemoteInfoByPortal.set(portal, remoteInfo);
|
|
82
82
|
}
|
|
83
83
|
indexFirst(syncGroupByExceptionSet, owner["exceptions"], owner);
|
|
84
84
|
indexFirst(targetByBuildPhase, owner["buildPhases"], owner);
|
|
85
|
-
const listId = asString(owner["buildConfigurationList"]);
|
|
85
|
+
const listId = asString$1(owner["buildConfigurationList"]);
|
|
86
86
|
if (listId != null && !configurationListOwners.has(listId)) configurationListOwners.set(listId, [ownerId, owner]);
|
|
87
87
|
}
|
|
88
88
|
/**
|
|
89
89
|
* The name an object displays as when nothing more specific applies:
|
|
90
90
|
* `name`, then `productName`, then `path`, then its isa.
|
|
91
91
|
*/
|
|
92
|
-
const defaultName = (object, isa) => asString(object["name"]) ?? asString(object["productName"]) ?? asString(object["path"]) ?? isa;
|
|
92
|
+
const defaultName = (object, isa) => asString$1(object["name"]) ?? asString$1(object["productName"]) ?? asString$1(object["path"]) ?? isa;
|
|
93
93
|
/**
|
|
94
94
|
* The display name of the synchronized folder an exception set belongs
|
|
95
95
|
* to, found through the sync group whose `exceptions` array lists it.
|
|
96
96
|
*/
|
|
97
97
|
const exceptionSetFolderName = (id) => {
|
|
98
98
|
const group = syncGroupByExceptionSet.get(id);
|
|
99
|
-
return group == null ? void 0 : asString(group["name"]) ?? asString(group["path"]);
|
|
99
|
+
return group == null ? void 0 : asString$1(group["name"]) ?? asString$1(group["path"]);
|
|
100
100
|
};
|
|
101
101
|
/**
|
|
102
102
|
* Comment for a `PBXFileSystemSynchronizedBuildFileExceptionSet`,
|
|
@@ -106,10 +106,10 @@ function createReferenceComments(root) {
|
|
|
106
106
|
*/
|
|
107
107
|
const buildFileExceptionSetComment = (id, set) => {
|
|
108
108
|
const folder = exceptionSetFolderName(id);
|
|
109
|
-
const targetId = asString(set["target"]);
|
|
109
|
+
const targetId = asString$1(set["target"]);
|
|
110
110
|
const target = targetId == null ? void 0 : objects[targetId];
|
|
111
111
|
if (folder == null || !isDictionary(target)) return;
|
|
112
|
-
const targetName = asString(target["name"]) ?? asString(target["productName"]) ?? asString(target["path"]);
|
|
112
|
+
const targetName = asString$1(target["name"]) ?? asString$1(target["productName"]) ?? asString$1(target["path"]);
|
|
113
113
|
return targetName == null ? void 0 : `Exceptions for "${folder}" folder in "${targetName}" target`;
|
|
114
114
|
};
|
|
115
115
|
/**
|
|
@@ -120,12 +120,12 @@ function createReferenceComments(root) {
|
|
|
120
120
|
*/
|
|
121
121
|
const membershipExceptionSetComment = (id, set) => {
|
|
122
122
|
const folder = exceptionSetFolderName(id);
|
|
123
|
-
const phaseId = asString(set["buildPhase"]);
|
|
123
|
+
const phaseId = asString$1(set["buildPhase"]);
|
|
124
124
|
const phase = phaseId == null ? void 0 : objects[phaseId];
|
|
125
125
|
if (folder == null || phaseId == null || !isDictionary(phase)) return;
|
|
126
|
-
const phaseName = asString(phase["name"]) ?? defaultBuildPhaseName(asString(phase["isa"]) ?? "");
|
|
126
|
+
const phaseName = asString$1(phase["name"]) ?? defaultBuildPhaseName(asString$1(phase["isa"]) ?? "");
|
|
127
127
|
const target = targetByBuildPhase.get(phaseId);
|
|
128
|
-
const targetName = target == null ? void 0 : asString(target["name"]) ?? asString(target["productName"]) ?? asString(target["path"]);
|
|
128
|
+
const targetName = target == null ? void 0 : asString$1(target["name"]) ?? asString$1(target["productName"]) ?? asString$1(target["path"]);
|
|
129
129
|
if (phaseName == null || targetName == null) return;
|
|
130
130
|
return `Exceptions for "${folder}" folder in "${phaseName}" phase from "${targetName}" target`;
|
|
131
131
|
};
|
|
@@ -136,7 +136,7 @@ function createReferenceComments(root) {
|
|
|
136
136
|
const buildFileComment = (id, buildFile) => {
|
|
137
137
|
const phase = fileToPhase.get(id);
|
|
138
138
|
const phaseName = phase == null ? "[missing build phase]" : phase.name ?? defaultBuildPhaseName(phase.isa) ?? "";
|
|
139
|
-
const refId = asString(buildFile["fileRef"]) ?? asString(buildFile["productRef"]);
|
|
139
|
+
const refId = asString$1(buildFile["fileRef"]) ?? asString$1(buildFile["productRef"]);
|
|
140
140
|
const referenced = refId == null ? void 0 : objects[refId];
|
|
141
141
|
return `${(refId != null && isDictionary(referenced) ? commentFor(refId, referenced) : void 0) ?? "(null)"} in ${phaseName}`;
|
|
142
142
|
};
|
|
@@ -148,15 +148,15 @@ function createReferenceComments(root) {
|
|
|
148
148
|
const ownerEntry = configurationListOwners.get(id);
|
|
149
149
|
if (ownerEntry == null) return "Build configuration list for [unknown]";
|
|
150
150
|
const [ownerId, owner] = ownerEntry;
|
|
151
|
-
const isa = asString(owner["isa"]) ?? "";
|
|
152
|
-
const ownName = asString(owner["name"]) ?? asString(owner["path"]) ?? asString(owner["productName"]);
|
|
151
|
+
const isa = asString$1(owner["isa"]) ?? "";
|
|
152
|
+
const ownName = asString$1(owner["name"]) ?? asString$1(owner["path"]) ?? asString$1(owner["productName"]);
|
|
153
153
|
if (ownName != null) return `Build configuration list for ${isa} "${ownName}"`;
|
|
154
154
|
const targets = owner["targets"];
|
|
155
155
|
if (Array.isArray(targets)) {
|
|
156
156
|
const firstTargetId = targets.find((target) => typeof target === "string");
|
|
157
157
|
const firstTarget = firstTargetId == null ? void 0 : objects[firstTargetId];
|
|
158
158
|
if (isDictionary(firstTarget)) {
|
|
159
|
-
const targetName = asString(firstTarget["productName"]) ?? asString(firstTarget["name"]);
|
|
159
|
+
const targetName = asString$1(firstTarget["productName"]) ?? asString$1(firstTarget["name"]);
|
|
160
160
|
if (targetName != null) return `Build configuration list for ${isa} "${targetName}"`;
|
|
161
161
|
}
|
|
162
162
|
}
|
|
@@ -172,7 +172,7 @@ function createReferenceComments(root) {
|
|
|
172
172
|
const commentFor = (id, object) => {
|
|
173
173
|
const cached = cache.get(id);
|
|
174
174
|
if (cached != null) return cached;
|
|
175
|
-
const isa = asString(object["isa"]);
|
|
175
|
+
const isa = asString$1(object["isa"]);
|
|
176
176
|
if (isa == null) return;
|
|
177
177
|
if (inProgress.has(id)) return;
|
|
178
178
|
inProgress.add(id);
|
|
@@ -180,17 +180,17 @@ function createReferenceComments(root) {
|
|
|
180
180
|
if (isa === "PBXBuildFile") comment = buildFileComment(id, object);
|
|
181
181
|
else if (isa === "XCConfigurationList") comment = configurationListComment(id);
|
|
182
182
|
else if (isa === "XCRemoteSwiftPackageReference") {
|
|
183
|
-
const repoUrl = asString(object["repositoryURL"]);
|
|
183
|
+
const repoUrl = asString$1(object["repositoryURL"]);
|
|
184
184
|
comment = repoUrl == null ? isa : `${isa} "${repoNameFromUrl(repoUrl)}"`;
|
|
185
185
|
} else if (isa === "XCLocalSwiftPackageReference") {
|
|
186
|
-
const relativePath = asString(object["relativePath"]);
|
|
186
|
+
const relativePath = asString$1(object["relativePath"]);
|
|
187
187
|
comment = relativePath == null ? isa : `${isa} "${relativePath}"`;
|
|
188
188
|
} else if (isa === "PBXProject") comment = "Project object";
|
|
189
189
|
else if (isa === "PBXFileSystemSynchronizedBuildFileExceptionSet") comment = buildFileExceptionSetComment(id, object) ?? isa;
|
|
190
190
|
else if (isa === "PBXFileSystemSynchronizedGroupBuildPhaseMembershipExceptionSet") comment = membershipExceptionSetComment(id, object) ?? isa;
|
|
191
191
|
else if (isa === "PBXTargetDependency") comment = isa;
|
|
192
|
-
else if (isa.endsWith("BuildPhase")) comment = asString(object["name"]) ?? defaultBuildPhaseName(isa) ?? "";
|
|
193
|
-
else if (isa === "PBXGroup" && asString(object["name"]) == null && asString(object["path"]) == null) comment = "";
|
|
192
|
+
else if (isa.endsWith("BuildPhase")) comment = asString$1(object["name"]) ?? defaultBuildPhaseName(isa) ?? "";
|
|
193
|
+
else if (isa === "PBXGroup" && asString$1(object["name"]) == null && asString$1(object["path"]) == null) comment = "";
|
|
194
194
|
else comment = defaultName(object, isa);
|
|
195
195
|
inProgress.delete(id);
|
|
196
196
|
cache.set(id, comment);
|
|
@@ -259,6 +259,25 @@ var PbxprojParseError = class extends Error {
|
|
|
259
259
|
}
|
|
260
260
|
};
|
|
261
261
|
/**
|
|
262
|
+
* Thrown by the object model when an operation cannot proceed: the
|
|
263
|
+
* document lacks the structure the operation needs (no objects dictionary,
|
|
264
|
+
* no root project object), a view's object was removed from the document,
|
|
265
|
+
* or a creation request names a product type the model cannot scaffold.
|
|
266
|
+
*
|
|
267
|
+
* Read paths in the model return `undefined` for merely malformed data;
|
|
268
|
+
* this error marks the cases where continuing would corrupt the document
|
|
269
|
+
* or hide a programming mistake.
|
|
270
|
+
*/
|
|
271
|
+
var XcodeModelError = class extends Error {
|
|
272
|
+
/**
|
|
273
|
+
* @param message Description of the failed operation.
|
|
274
|
+
*/
|
|
275
|
+
constructor(message) {
|
|
276
|
+
super(message);
|
|
277
|
+
this.name = "XcodeModelError";
|
|
278
|
+
}
|
|
279
|
+
};
|
|
280
|
+
/**
|
|
262
281
|
* Thrown when a value cannot be represented in a `project.pbxproj` document.
|
|
263
282
|
*
|
|
264
283
|
* Raised for `null`, `undefined`, booleans, bigints, functions, symbols,
|
|
@@ -727,6 +746,578 @@ function buildPbxproj(root) {
|
|
|
727
746
|
return new Writer(root).toString();
|
|
728
747
|
}
|
|
729
748
|
//#endregion
|
|
749
|
+
//#region src/model/isa.ts
|
|
750
|
+
/**
|
|
751
|
+
* Names and well-known values of the pbxproj object vocabulary.
|
|
752
|
+
*
|
|
753
|
+
* Everything here mirrors strings Xcode itself writes; nothing is invented.
|
|
754
|
+
* Centralizing them keeps the object model free of string literals and
|
|
755
|
+
* gives call sites one place to import from.
|
|
756
|
+
*
|
|
757
|
+
* @module
|
|
758
|
+
*/
|
|
759
|
+
/**
|
|
760
|
+
* The `isa` names the object model works with. The parser and serializer
|
|
761
|
+
* accept any isa; this list only covers the kinds the model creates or
|
|
762
|
+
* gives typed access to.
|
|
763
|
+
*/
|
|
764
|
+
const Isa = {
|
|
765
|
+
aggregateTarget: "PBXAggregateTarget",
|
|
766
|
+
buildFile: "PBXBuildFile",
|
|
767
|
+
buildRule: "PBXBuildRule",
|
|
768
|
+
containerItemProxy: "PBXContainerItemProxy",
|
|
769
|
+
copyFilesBuildPhase: "PBXCopyFilesBuildPhase",
|
|
770
|
+
fileReference: "PBXFileReference",
|
|
771
|
+
fileSystemSynchronizedBuildFileExceptionSet: "PBXFileSystemSynchronizedBuildFileExceptionSet",
|
|
772
|
+
fileSystemSynchronizedRootGroup: "PBXFileSystemSynchronizedRootGroup",
|
|
773
|
+
frameworksBuildPhase: "PBXFrameworksBuildPhase",
|
|
774
|
+
group: "PBXGroup",
|
|
775
|
+
legacyTarget: "PBXLegacyTarget",
|
|
776
|
+
nativeTarget: "PBXNativeTarget",
|
|
777
|
+
project: "PBXProject",
|
|
778
|
+
referenceProxy: "PBXReferenceProxy",
|
|
779
|
+
resourcesBuildPhase: "PBXResourcesBuildPhase",
|
|
780
|
+
sourcesBuildPhase: "PBXSourcesBuildPhase",
|
|
781
|
+
targetDependency: "PBXTargetDependency",
|
|
782
|
+
variantGroup: "PBXVariantGroup",
|
|
783
|
+
buildConfiguration: "XCBuildConfiguration",
|
|
784
|
+
configurationList: "XCConfigurationList",
|
|
785
|
+
localSwiftPackageReference: "XCLocalSwiftPackageReference",
|
|
786
|
+
remoteSwiftPackageReference: "XCRemoteSwiftPackageReference",
|
|
787
|
+
swiftPackageProductDependency: "XCSwiftPackageProductDependency",
|
|
788
|
+
versionGroup: "XCVersionGroup"
|
|
789
|
+
};
|
|
790
|
+
/**
|
|
791
|
+
* Product type identifiers of the targets the model creates or reasons
|
|
792
|
+
* about. Other product types pass through untouched.
|
|
793
|
+
*/
|
|
794
|
+
const ProductType = {
|
|
795
|
+
application: "com.apple.product-type.application",
|
|
796
|
+
messagesApplication: "com.apple.product-type.application.messages",
|
|
797
|
+
appExtension: "com.apple.product-type.app-extension",
|
|
798
|
+
messagesExtension: "com.apple.product-type.app-extension.messages",
|
|
799
|
+
extensionKitExtension: "com.apple.product-type.extensionkit-extension",
|
|
800
|
+
onDemandInstallCapableApplication: "com.apple.product-type.application.on-demand-install-capable",
|
|
801
|
+
watchApp: "com.apple.product-type.application.watchapp2"
|
|
802
|
+
};
|
|
803
|
+
/**
|
|
804
|
+
* How a product type's build product appears on disk. Each entry carries
|
|
805
|
+
* the wrapper file extension and the `explicitFileType` of the product
|
|
806
|
+
* file reference.
|
|
807
|
+
*/
|
|
808
|
+
const PRODUCT_FILE_INFO = {
|
|
809
|
+
[ProductType.application]: {
|
|
810
|
+
extension: ".app",
|
|
811
|
+
fileType: "wrapper.application"
|
|
812
|
+
},
|
|
813
|
+
[ProductType.messagesApplication]: {
|
|
814
|
+
extension: ".app",
|
|
815
|
+
fileType: "wrapper.application"
|
|
816
|
+
},
|
|
817
|
+
[ProductType.onDemandInstallCapableApplication]: {
|
|
818
|
+
extension: ".app",
|
|
819
|
+
fileType: "wrapper.application"
|
|
820
|
+
},
|
|
821
|
+
[ProductType.watchApp]: {
|
|
822
|
+
extension: ".app",
|
|
823
|
+
fileType: "wrapper.application"
|
|
824
|
+
},
|
|
825
|
+
[ProductType.appExtension]: {
|
|
826
|
+
extension: ".appex",
|
|
827
|
+
fileType: "wrapper.app-extension"
|
|
828
|
+
},
|
|
829
|
+
[ProductType.messagesExtension]: {
|
|
830
|
+
extension: ".appex",
|
|
831
|
+
fileType: "wrapper.app-extension"
|
|
832
|
+
},
|
|
833
|
+
[ProductType.extensionKitExtension]: {
|
|
834
|
+
extension: ".appex",
|
|
835
|
+
fileType: "wrapper.app-extension"
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
/**
|
|
839
|
+
* The build-setting key that carries each platform's deployment target.
|
|
840
|
+
* A target (or its project) sets exactly one of these per platform it
|
|
841
|
+
* builds for, which is what makes them usable as a platform signal.
|
|
842
|
+
*/
|
|
843
|
+
const DEPLOYMENT_TARGET_KEY = {
|
|
844
|
+
ios: "IPHONEOS_DEPLOYMENT_TARGET",
|
|
845
|
+
macos: "MACOSX_DEPLOYMENT_TARGET",
|
|
846
|
+
tvos: "TVOS_DEPLOYMENT_TARGET",
|
|
847
|
+
watchos: "WATCHOS_DEPLOYMENT_TARGET",
|
|
848
|
+
visionos: "XROS_DEPLOYMENT_TARGET"
|
|
849
|
+
};
|
|
850
|
+
/**
|
|
851
|
+
* `dstSubfolderSpec` values for copy-files build phases, named after the
|
|
852
|
+
* destination each selects.
|
|
853
|
+
*/
|
|
854
|
+
const CopyFilesDestination = {
|
|
855
|
+
plugins: 13,
|
|
856
|
+
productsDirectory: 16
|
|
857
|
+
};
|
|
858
|
+
/**
|
|
859
|
+
* Resolves the embed phase destination for an extension-like product type.
|
|
860
|
+
* Watch applications are embedded by product type here; callers that only
|
|
861
|
+
* know build settings should check the deployment-target key instead.
|
|
862
|
+
*/
|
|
863
|
+
function embedDestinationFor(productType) {
|
|
864
|
+
switch (productType) {
|
|
865
|
+
case ProductType.onDemandInstallCapableApplication: return {
|
|
866
|
+
phaseName: "Embed App Clips",
|
|
867
|
+
dstSubfolderSpec: CopyFilesDestination.productsDirectory,
|
|
868
|
+
dstPath: "$(CONTENTS_FOLDER_PATH)/AppClips"
|
|
869
|
+
};
|
|
870
|
+
case ProductType.watchApp:
|
|
871
|
+
case ProductType.application: return {
|
|
872
|
+
phaseName: "Embed Watch Content",
|
|
873
|
+
dstSubfolderSpec: CopyFilesDestination.productsDirectory,
|
|
874
|
+
dstPath: "$(CONTENTS_FOLDER_PATH)/Watch"
|
|
875
|
+
};
|
|
876
|
+
case ProductType.extensionKitExtension: return {
|
|
877
|
+
phaseName: "Embed ExtensionKit Extensions",
|
|
878
|
+
dstSubfolderSpec: CopyFilesDestination.productsDirectory,
|
|
879
|
+
dstPath: "$(EXTENSIONS_FOLDER_PATH)"
|
|
880
|
+
};
|
|
881
|
+
default: return {
|
|
882
|
+
phaseName: "Embed Foundation Extensions",
|
|
883
|
+
dstSubfolderSpec: CopyFilesDestination.plugins,
|
|
884
|
+
dstPath: ""
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
/**
|
|
889
|
+
* `lastKnownFileType` values for file references created by path, keyed by
|
|
890
|
+
* file extension. Extensions outside the map create references without a
|
|
891
|
+
* type, which Xcode tolerates and re-derives.
|
|
892
|
+
*/
|
|
893
|
+
const FILE_TYPE_BY_EXTENSION = {
|
|
894
|
+
".swift": "sourcecode.swift",
|
|
895
|
+
".m": "sourcecode.c.objc",
|
|
896
|
+
".mm": "sourcecode.cpp.objcpp",
|
|
897
|
+
".h": "sourcecode.c.h",
|
|
898
|
+
".c": "sourcecode.c.c",
|
|
899
|
+
".cpp": "sourcecode.cpp.cpp",
|
|
900
|
+
".plist": "text.plist.xml",
|
|
901
|
+
".entitlements": "text.plist.entitlements",
|
|
902
|
+
".xcconfig": "text.xcconfig",
|
|
903
|
+
".storyboard": "file.storyboard",
|
|
904
|
+
".xib": "file.xib",
|
|
905
|
+
".xcassets": "folder.assetcatalog",
|
|
906
|
+
".framework": "wrapper.framework",
|
|
907
|
+
".xcdatamodeld": "wrapper.xcdatamodeld",
|
|
908
|
+
".metal": "sourcecode.metal",
|
|
909
|
+
".md": "net.daringfireball.markdown",
|
|
910
|
+
".json": "text.json"
|
|
911
|
+
};
|
|
912
|
+
//#endregion
|
|
913
|
+
//#region src/model/values.ts
|
|
914
|
+
/**
|
|
915
|
+
* Returns the value when it is a string, and `undefined` otherwise.
|
|
916
|
+
*/
|
|
917
|
+
function asString(value) {
|
|
918
|
+
return typeof value === "string" ? value : void 0;
|
|
919
|
+
}
|
|
920
|
+
/**
|
|
921
|
+
* Returns the value when it is a dictionary, and `undefined` otherwise.
|
|
922
|
+
*
|
|
923
|
+
* Arrays and `Uint8Array` data are objects at runtime too, so a bare
|
|
924
|
+
* `typeof` check is not enough.
|
|
925
|
+
*/
|
|
926
|
+
function asDictionary(value) {
|
|
927
|
+
if (typeof value === "object" && value !== null && !Array.isArray(value) && !(value instanceof Uint8Array)) return value;
|
|
928
|
+
}
|
|
929
|
+
/**
|
|
930
|
+
* Returns the array under `key`, creating an empty one when the key is
|
|
931
|
+
* absent or holds a non-array value.
|
|
932
|
+
*
|
|
933
|
+
* Mutating methods use this to append into list fields (`children`,
|
|
934
|
+
* `dependencies`, `exceptions`) without separate existence checks at every
|
|
935
|
+
* call site.
|
|
936
|
+
*/
|
|
937
|
+
function ensureArray(object, key) {
|
|
938
|
+
const existing = object[key];
|
|
939
|
+
if (Array.isArray(existing)) return existing;
|
|
940
|
+
const created = [];
|
|
941
|
+
object[key] = created;
|
|
942
|
+
return created;
|
|
943
|
+
}
|
|
944
|
+
/**
|
|
945
|
+
* Collects the string items of a possibly absent, possibly mixed array.
|
|
946
|
+
*
|
|
947
|
+
* Reference lists in well-formed documents contain only id strings, but a
|
|
948
|
+
* malformed document can mix in anything; non-strings are skipped rather
|
|
949
|
+
* than thrown on, matching the library's soft-failure stance on reads.
|
|
950
|
+
*/
|
|
951
|
+
function stringItems(value) {
|
|
952
|
+
if (!Array.isArray(value)) return [];
|
|
953
|
+
const items = [];
|
|
954
|
+
for (const item of value) if (typeof item === "string") items.push(item);
|
|
955
|
+
return items;
|
|
956
|
+
}
|
|
957
|
+
//#endregion
|
|
958
|
+
//#region src/model/object.ts
|
|
959
|
+
/**
|
|
960
|
+
* The base view class of the object model.
|
|
961
|
+
*
|
|
962
|
+
* Views are lightweight, identity-mapped facades over entries of the
|
|
963
|
+
* document's `objects` dictionary. All state lives in the parsed document
|
|
964
|
+
* itself; a view holds only its id and a reference back to the project, so
|
|
965
|
+
* document text produced from the model reflects every mutation with no
|
|
966
|
+
* separate serialization step.
|
|
967
|
+
*
|
|
968
|
+
* @module
|
|
969
|
+
*/
|
|
970
|
+
/**
|
|
971
|
+
* A typed handle on one object of the document.
|
|
972
|
+
*
|
|
973
|
+
* Two lookups of the same id return the same view instance (the project
|
|
974
|
+
* keeps an identity map), so views compare with `===`.
|
|
975
|
+
*
|
|
976
|
+
* The type parameter describes the properties of a well-formed object of
|
|
977
|
+
* this kind. Subclasses fix it to their kind's interface, which gives
|
|
978
|
+
* `properties` autocompletion. Treat the shape as a description rather
|
|
979
|
+
* than a guarantee. A malformed document can hold anything, so the model
|
|
980
|
+
* itself always reads through the narrowing accessors.
|
|
981
|
+
*/
|
|
982
|
+
var XcodeObject = class {
|
|
983
|
+
/** The project this object belongs to. */
|
|
984
|
+
project;
|
|
985
|
+
/** The object's 24-character identifier (its key in `objects`). */
|
|
986
|
+
id;
|
|
987
|
+
/**
|
|
988
|
+
* Views are created by the project's identity map; use
|
|
989
|
+
* {@link XcodeProject.get} or a typed query instead of constructing
|
|
990
|
+
* views directly.
|
|
991
|
+
*/
|
|
992
|
+
constructor(project, id) {
|
|
993
|
+
this.project = project;
|
|
994
|
+
this.id = id;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* The object's raw dictionary inside the document. Mutations through the
|
|
998
|
+
* model write here, and direct writes are equally valid; the model adds
|
|
999
|
+
* no caching over these properties.
|
|
1000
|
+
*
|
|
1001
|
+
* The typed shape is asserted, not checked. It describes what a
|
|
1002
|
+
* well-formed object of this kind carries (see `properties.ts`).
|
|
1003
|
+
*/
|
|
1004
|
+
get properties() {
|
|
1005
|
+
return this.project.propertiesOf(this.id);
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* The object's `isa` kind name, or the empty string when the field is
|
|
1009
|
+
* missing or malformed.
|
|
1010
|
+
*/
|
|
1011
|
+
get isa() {
|
|
1012
|
+
return asString(this.properties["isa"]) ?? "";
|
|
1013
|
+
}
|
|
1014
|
+
/**
|
|
1015
|
+
* Reads a property expected to be a string. Returns `undefined` when the
|
|
1016
|
+
* property is absent or holds another type.
|
|
1017
|
+
*/
|
|
1018
|
+
getString(key) {
|
|
1019
|
+
return asString(this.properties[key]);
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Writes one property of the object.
|
|
1023
|
+
*
|
|
1024
|
+
* The write goes through the untyped document dictionary. TypeScript
|
|
1025
|
+
* does not allow writes through a generic index, and the typed shape on
|
|
1026
|
+
* `properties` is descriptive anyway.
|
|
1027
|
+
*/
|
|
1028
|
+
set(key, value) {
|
|
1029
|
+
this.project.propertiesOf(this.id)[key] = value;
|
|
1030
|
+
}
|
|
1031
|
+
/**
|
|
1032
|
+
* The views of the objects referenced by an id-list property, resolved in
|
|
1033
|
+
* list order. Dangling ids and non-string entries of malformed documents
|
|
1034
|
+
* are skipped.
|
|
1035
|
+
*/
|
|
1036
|
+
referencedViews(key) {
|
|
1037
|
+
const items = this.properties[key];
|
|
1038
|
+
if (!Array.isArray(items)) return [];
|
|
1039
|
+
const views = [];
|
|
1040
|
+
for (const item of items) {
|
|
1041
|
+
const view = typeof item === "string" ? this.project.get(item) : void 0;
|
|
1042
|
+
if (view != null) views.push(view);
|
|
1043
|
+
}
|
|
1044
|
+
return views;
|
|
1045
|
+
}
|
|
1046
|
+
/**
|
|
1047
|
+
* Removes the object from the document and strips every reference to it:
|
|
1048
|
+
* string properties naming the id are deleted, id lists drop it, and
|
|
1049
|
+
* nested dictionaries keyed by object id (such as the root project's
|
|
1050
|
+
* `TargetAttributes`) drop its entry.
|
|
1051
|
+
*
|
|
1052
|
+
* This is the low-level removal; it does not cascade to objects that only
|
|
1053
|
+
* made sense alongside this one. Higher-level operations like
|
|
1054
|
+
* {@link XcodeProject.removeTarget} compose it into full teardowns.
|
|
1055
|
+
*/
|
|
1056
|
+
removeFromProject() {
|
|
1057
|
+
this.project.removeObject(this.id);
|
|
1058
|
+
}
|
|
1059
|
+
};
|
|
1060
|
+
//#endregion
|
|
1061
|
+
//#region src/model/objects.ts
|
|
1062
|
+
/**
|
|
1063
|
+
* Typed views over the document object kinds that are not targets, from
|
|
1064
|
+
* groups and build phases to version groups and reference proxies.
|
|
1065
|
+
*
|
|
1066
|
+
* Each class adds the accessors and mutations its kind supports; everything
|
|
1067
|
+
* ultimately reads and writes the raw dictionaries through the base class,
|
|
1068
|
+
* so mixing model calls with direct property access stays safe.
|
|
1069
|
+
*
|
|
1070
|
+
* @module
|
|
1071
|
+
*/
|
|
1072
|
+
/**
|
|
1073
|
+
* A `PBXGroup` is a folder in Xcode's navigator, holding references to
|
|
1074
|
+
* files and other groups. Variant groups (`PBXVariantGroup`) share this
|
|
1075
|
+
* view. The type parameter lets subclasses carry a more specific property
|
|
1076
|
+
* shape.
|
|
1077
|
+
*/
|
|
1078
|
+
var Group = class Group extends XcodeObject {
|
|
1079
|
+
/**
|
|
1080
|
+
* Ids of the group's children, in navigator order. Non-string entries of
|
|
1081
|
+
* a malformed document are skipped.
|
|
1082
|
+
*/
|
|
1083
|
+
get childIds() {
|
|
1084
|
+
return stringItems(this.properties["children"]);
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* The views of the group's children, in navigator order.
|
|
1088
|
+
*/
|
|
1089
|
+
children() {
|
|
1090
|
+
return this.referencedViews("children");
|
|
1091
|
+
}
|
|
1092
|
+
/**
|
|
1093
|
+
* Adds an existing object (a file reference or another group) to the end
|
|
1094
|
+
* of the group's children. Adding a child the group already lists is a
|
|
1095
|
+
* no-op.
|
|
1096
|
+
*/
|
|
1097
|
+
addChild(child) {
|
|
1098
|
+
const children = ensureArray(this.properties, "children");
|
|
1099
|
+
if (!children.includes(child.id)) children.push(child.id);
|
|
1100
|
+
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Returns the descendant group for a `/`-separated path, creating any
|
|
1103
|
+
* missing groups along the way. Each component matches a child group by
|
|
1104
|
+
* its `path`, falling back to its `name`; created groups carry the
|
|
1105
|
+
* component as their `path` so they mirror folders on disk.
|
|
1106
|
+
*
|
|
1107
|
+
* ```ts
|
|
1108
|
+
* const generated = mainGroup.ensureGroup("Sources/Generated");
|
|
1109
|
+
* generated.createFile("Config.swift");
|
|
1110
|
+
* ```
|
|
1111
|
+
*/
|
|
1112
|
+
ensureGroup(path) {
|
|
1113
|
+
return path.split("/").filter((component) => component !== "").reduce((parent, component) => {
|
|
1114
|
+
const existing = parent.children().find((child) => child instanceof Group && (child.getString("path") ?? child.getString("name")) === component);
|
|
1115
|
+
if (existing != null) return existing;
|
|
1116
|
+
const created = this.project.add(Isa.group, {
|
|
1117
|
+
children: [],
|
|
1118
|
+
path: component,
|
|
1119
|
+
sourceTree: "<group>"
|
|
1120
|
+
}, `${Isa.group} ${parent.id} ${component}`);
|
|
1121
|
+
parent.addChild(created);
|
|
1122
|
+
return created;
|
|
1123
|
+
}, this);
|
|
1124
|
+
}
|
|
1125
|
+
/**
|
|
1126
|
+
* Creates a `PBXFileReference` for a path relative to this group and adds
|
|
1127
|
+
* it to the group's children.
|
|
1128
|
+
*
|
|
1129
|
+
* The reference's `lastKnownFileType` derives from the file extension
|
|
1130
|
+
* when it is a known kind; otherwise the reference carries no type and
|
|
1131
|
+
* Xcode re-derives one on open.
|
|
1132
|
+
*
|
|
1133
|
+
* @param path File path relative to the group, for example
|
|
1134
|
+
* `Demo/Config.swift`.
|
|
1135
|
+
* @returns The view of the created file reference.
|
|
1136
|
+
*/
|
|
1137
|
+
createFile(path) {
|
|
1138
|
+
const extensionStart = path.lastIndexOf(".");
|
|
1139
|
+
const fileType = extensionStart === -1 ? void 0 : FILE_TYPE_BY_EXTENSION[path.slice(extensionStart)];
|
|
1140
|
+
const reference = this.project.add(Isa.fileReference, {
|
|
1141
|
+
...fileType == null ? {} : { lastKnownFileType: fileType },
|
|
1142
|
+
path,
|
|
1143
|
+
sourceTree: "<group>"
|
|
1144
|
+
}, `${Isa.fileReference} ${path}`);
|
|
1145
|
+
this.addChild(reference);
|
|
1146
|
+
return reference;
|
|
1147
|
+
}
|
|
1148
|
+
};
|
|
1149
|
+
/**
|
|
1150
|
+
* A build phase is an ordered list of build files processed by one step
|
|
1151
|
+
* of a target's build. Every `PBX*BuildPhase` kind shares this view.
|
|
1152
|
+
*/
|
|
1153
|
+
var BuildPhase = class extends XcodeObject {
|
|
1154
|
+
/**
|
|
1155
|
+
* The phase's display name, when it carries an explicit one. Xcode names
|
|
1156
|
+
* copy-files and shell-script phases; the standard phases derive their
|
|
1157
|
+
* names from their isa.
|
|
1158
|
+
*/
|
|
1159
|
+
get name() {
|
|
1160
|
+
return this.getString("name");
|
|
1161
|
+
}
|
|
1162
|
+
/**
|
|
1163
|
+
* Ids of the phase's `PBXBuildFile` entries, in build order.
|
|
1164
|
+
*/
|
|
1165
|
+
get buildFileIds() {
|
|
1166
|
+
return stringItems(this.properties["files"]);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Whether the phase already lists the build file.
|
|
1170
|
+
*/
|
|
1171
|
+
containsBuildFile(buildFileId) {
|
|
1172
|
+
return this.buildFileIds.includes(buildFileId);
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Appends an existing build file to the phase unless it is already
|
|
1176
|
+
* listed.
|
|
1177
|
+
*/
|
|
1178
|
+
appendBuildFile(buildFileId) {
|
|
1179
|
+
const files = ensureArray(this.properties, "files");
|
|
1180
|
+
if (!files.includes(buildFileId)) files.push(buildFileId);
|
|
1181
|
+
}
|
|
1182
|
+
/**
|
|
1183
|
+
* Removes a build file from the phase. Removing an id the phase does not
|
|
1184
|
+
* list is a no-op.
|
|
1185
|
+
*/
|
|
1186
|
+
removeBuildFile(buildFileId) {
|
|
1187
|
+
const files = this.properties["files"];
|
|
1188
|
+
if (Array.isArray(files)) this.properties["files"] = files.filter((file) => file !== buildFileId);
|
|
1189
|
+
}
|
|
1190
|
+
/**
|
|
1191
|
+
* Ensures the phase carries a `PBXBuildFile` for the referenced object,
|
|
1192
|
+
* creating one when no existing build file in this phase points at it.
|
|
1193
|
+
*
|
|
1194
|
+
* @param reference The file reference or package product the build file
|
|
1195
|
+
* should point at.
|
|
1196
|
+
* @param options.referenceKey Which build-file field carries the
|
|
1197
|
+
* reference: `fileRef` for file references (the default) or
|
|
1198
|
+
* `productRef` for Swift package products.
|
|
1199
|
+
* @param options.settings Optional per-file settings, for example
|
|
1200
|
+
* `{ ATTRIBUTES: ["RemoveHeadersOnCopy"] }`.
|
|
1201
|
+
* @returns The view of the phase's build file for the reference.
|
|
1202
|
+
*/
|
|
1203
|
+
ensureBuildFile(reference, options = {}) {
|
|
1204
|
+
const referenceKey = options.referenceKey ?? "fileRef";
|
|
1205
|
+
for (const buildFileId of this.buildFileIds) {
|
|
1206
|
+
const existing = this.project.get(buildFileId);
|
|
1207
|
+
if (existing?.getString(referenceKey) === reference.id) return existing;
|
|
1208
|
+
}
|
|
1209
|
+
const buildFile = this.project.add(Isa.buildFile, {
|
|
1210
|
+
[referenceKey]: reference.id,
|
|
1211
|
+
...options.settings == null ? {} : { settings: options.settings }
|
|
1212
|
+
}, `${Isa.buildFile} ${this.id} ${reference.id}`);
|
|
1213
|
+
this.appendBuildFile(buildFile.id);
|
|
1214
|
+
return buildFile;
|
|
1215
|
+
}
|
|
1216
|
+
};
|
|
1217
|
+
/**
|
|
1218
|
+
* A `PBXFileSystemSynchronizedRootGroup` is an Xcode 16 folder whose
|
|
1219
|
+
* members are synchronized from disk instead of listed individually.
|
|
1220
|
+
*/
|
|
1221
|
+
var SyncRootGroup = class extends XcodeObject {
|
|
1222
|
+
/**
|
|
1223
|
+
* The group's on-disk folder path, when present.
|
|
1224
|
+
*/
|
|
1225
|
+
get path() {
|
|
1226
|
+
return this.getString("path");
|
|
1227
|
+
}
|
|
1228
|
+
/**
|
|
1229
|
+
* Excludes files from a target's membership in this synchronized folder
|
|
1230
|
+
* through a `PBXFileSystemSynchronizedBuildFileExceptionSet` linked into
|
|
1231
|
+
* the group's exceptions.
|
|
1232
|
+
*
|
|
1233
|
+
* Xcode keeps one exception set per target and folder, so when this
|
|
1234
|
+
* group already carries a set for the target, the file names merge into
|
|
1235
|
+
* it instead of creating a second set; names already excluded are not
|
|
1236
|
+
* duplicated.
|
|
1237
|
+
*
|
|
1238
|
+
* The standard use is keeping a scaffolded `Info.plist` from being
|
|
1239
|
+
* double-copied: the build already processes it through the target's
|
|
1240
|
+
* `INFOPLIST_FILE` setting.
|
|
1241
|
+
*
|
|
1242
|
+
* @param target The target whose membership the exceptions restrict.
|
|
1243
|
+
* @param membershipExceptions File names inside the folder to exclude.
|
|
1244
|
+
* @returns The view of the target's exception set for this folder.
|
|
1245
|
+
*/
|
|
1246
|
+
addMembershipExceptions(target, membershipExceptions) {
|
|
1247
|
+
for (const id of stringItems(this.properties["exceptions"])) {
|
|
1248
|
+
const existing = this.project.get(id);
|
|
1249
|
+
if (existing?.isa === Isa.fileSystemSynchronizedBuildFileExceptionSet && existing.getString("target") === target.id) {
|
|
1250
|
+
const names = ensureArray(existing.properties, "membershipExceptions");
|
|
1251
|
+
for (const name of membershipExceptions) if (!names.includes(name)) names.push(name);
|
|
1252
|
+
return existing;
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
1255
|
+
const exceptionSet = this.project.add(Isa.fileSystemSynchronizedBuildFileExceptionSet, {
|
|
1256
|
+
membershipExceptions,
|
|
1257
|
+
target: target.id
|
|
1258
|
+
}, `${Isa.fileSystemSynchronizedBuildFileExceptionSet} ${this.id} ${target.id}`);
|
|
1259
|
+
ensureArray(this.properties, "exceptions").push(exceptionSet.id);
|
|
1260
|
+
return exceptionSet;
|
|
1261
|
+
}
|
|
1262
|
+
};
|
|
1263
|
+
/**
|
|
1264
|
+
* A `PBXBuildRule` tells a target which compiler or script processes a
|
|
1265
|
+
* kind of file.
|
|
1266
|
+
*/
|
|
1267
|
+
var BuildRule = class extends XcodeObject {
|
|
1268
|
+
/**
|
|
1269
|
+
* The rule's script, when it is a script rule rather than a reference
|
|
1270
|
+
* to a compiler specification.
|
|
1271
|
+
*/
|
|
1272
|
+
get script() {
|
|
1273
|
+
return this.getString("script");
|
|
1274
|
+
}
|
|
1275
|
+
};
|
|
1276
|
+
/**
|
|
1277
|
+
* An `XCVersionGroup` contains a versioned Core Data model
|
|
1278
|
+
* (`.xcdatamodeld`). Its children are the model versions and
|
|
1279
|
+
* `currentVersion` names the active one.
|
|
1280
|
+
*/
|
|
1281
|
+
var VersionGroup = class extends Group {
|
|
1282
|
+
/**
|
|
1283
|
+
* The view of the active model version's file reference, when the group
|
|
1284
|
+
* names one.
|
|
1285
|
+
*/
|
|
1286
|
+
currentVersion() {
|
|
1287
|
+
const id = this.getString("currentVersion");
|
|
1288
|
+
return id == null ? void 0 : this.project.get(id);
|
|
1289
|
+
}
|
|
1290
|
+
/**
|
|
1291
|
+
* Makes a model version the active one, adding it to the group's
|
|
1292
|
+
* children when it is not listed yet.
|
|
1293
|
+
*/
|
|
1294
|
+
setCurrentVersion(reference) {
|
|
1295
|
+
this.addChild(reference);
|
|
1296
|
+
this.properties["currentVersion"] = reference.id;
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
/**
|
|
1300
|
+
* A `PBXReferenceProxy` stands in for a product built by a target of
|
|
1301
|
+
* another project referenced from this one.
|
|
1302
|
+
*/
|
|
1303
|
+
var ReferenceProxy = class extends XcodeObject {
|
|
1304
|
+
/**
|
|
1305
|
+
* The proxy's product path inside the other project's build directory,
|
|
1306
|
+
* when present.
|
|
1307
|
+
*/
|
|
1308
|
+
get path() {
|
|
1309
|
+
return this.getString("path");
|
|
1310
|
+
}
|
|
1311
|
+
/**
|
|
1312
|
+
* The view of the container item proxy that names the remote target,
|
|
1313
|
+
* when the reference resolves.
|
|
1314
|
+
*/
|
|
1315
|
+
remoteReference() {
|
|
1316
|
+
const id = this.getString("remoteRef");
|
|
1317
|
+
return id == null ? void 0 : this.project.get(id);
|
|
1318
|
+
}
|
|
1319
|
+
};
|
|
1320
|
+
//#endregion
|
|
730
1321
|
//#region src/escape.ts
|
|
731
1322
|
/**
|
|
732
1323
|
* Escape handling for quoted strings.
|
|
@@ -1373,4 +1964,1437 @@ function parsePbxproj(text) {
|
|
|
1373
1964
|
return new Parser(text).parseDocument();
|
|
1374
1965
|
}
|
|
1375
1966
|
//#endregion
|
|
1376
|
-
|
|
1967
|
+
//#region src/md5.ts
|
|
1968
|
+
/**
|
|
1969
|
+
* Embedded MD5, implemented from RFC 1321.
|
|
1970
|
+
*
|
|
1971
|
+
* Deterministic object ids hash their seed text (see `uuid.ts`), and the
|
|
1972
|
+
* library runs in every JavaScript runtime without depending on a crypto
|
|
1973
|
+
* module, so the digest is implemented here. MD5 is used strictly as a
|
|
1974
|
+
* stable text-to-bits mapping for identifier generation; nothing security
|
|
1975
|
+
* relevant derives from it.
|
|
1976
|
+
*
|
|
1977
|
+
* @module
|
|
1978
|
+
*/
|
|
1979
|
+
/**
|
|
1980
|
+
* Per-round left-rotation amounts, in the order the 64 steps apply them
|
|
1981
|
+
* (RFC 1321 section 3.4).
|
|
1982
|
+
*/
|
|
1983
|
+
const SHIFTS = [
|
|
1984
|
+
7,
|
|
1985
|
+
12,
|
|
1986
|
+
17,
|
|
1987
|
+
22,
|
|
1988
|
+
7,
|
|
1989
|
+
12,
|
|
1990
|
+
17,
|
|
1991
|
+
22,
|
|
1992
|
+
7,
|
|
1993
|
+
12,
|
|
1994
|
+
17,
|
|
1995
|
+
22,
|
|
1996
|
+
7,
|
|
1997
|
+
12,
|
|
1998
|
+
17,
|
|
1999
|
+
22,
|
|
2000
|
+
5,
|
|
2001
|
+
9,
|
|
2002
|
+
14,
|
|
2003
|
+
20,
|
|
2004
|
+
5,
|
|
2005
|
+
9,
|
|
2006
|
+
14,
|
|
2007
|
+
20,
|
|
2008
|
+
5,
|
|
2009
|
+
9,
|
|
2010
|
+
14,
|
|
2011
|
+
20,
|
|
2012
|
+
5,
|
|
2013
|
+
9,
|
|
2014
|
+
14,
|
|
2015
|
+
20,
|
|
2016
|
+
4,
|
|
2017
|
+
11,
|
|
2018
|
+
16,
|
|
2019
|
+
23,
|
|
2020
|
+
4,
|
|
2021
|
+
11,
|
|
2022
|
+
16,
|
|
2023
|
+
23,
|
|
2024
|
+
4,
|
|
2025
|
+
11,
|
|
2026
|
+
16,
|
|
2027
|
+
23,
|
|
2028
|
+
4,
|
|
2029
|
+
11,
|
|
2030
|
+
16,
|
|
2031
|
+
23,
|
|
2032
|
+
6,
|
|
2033
|
+
10,
|
|
2034
|
+
15,
|
|
2035
|
+
21,
|
|
2036
|
+
6,
|
|
2037
|
+
10,
|
|
2038
|
+
15,
|
|
2039
|
+
21,
|
|
2040
|
+
6,
|
|
2041
|
+
10,
|
|
2042
|
+
15,
|
|
2043
|
+
21,
|
|
2044
|
+
6,
|
|
2045
|
+
10,
|
|
2046
|
+
15,
|
|
2047
|
+
21
|
|
2048
|
+
];
|
|
2049
|
+
/**
|
|
2050
|
+
* Step constants for the 64 steps: the integer parts of
|
|
2051
|
+
* `abs(sin(i + 1)) * 2^32`, as RFC 1321 section 3.4 tabulates them. The
|
|
2052
|
+
* values are fixed by the specification rather than derived through
|
|
2053
|
+
* `Math.sin` at load, because the digest feeds deterministic identifiers
|
|
2054
|
+
* and floating-point transcendentals are the one spot where runtimes could
|
|
2055
|
+
* legally disagree.
|
|
2056
|
+
*/
|
|
2057
|
+
const SINES = new Uint32Array([
|
|
2058
|
+
3614090360,
|
|
2059
|
+
3905402710,
|
|
2060
|
+
606105819,
|
|
2061
|
+
3250441966,
|
|
2062
|
+
4118548399,
|
|
2063
|
+
1200080426,
|
|
2064
|
+
2821735955,
|
|
2065
|
+
4249261313,
|
|
2066
|
+
1770035416,
|
|
2067
|
+
2336552879,
|
|
2068
|
+
4294925233,
|
|
2069
|
+
2304563134,
|
|
2070
|
+
1804603682,
|
|
2071
|
+
4254626195,
|
|
2072
|
+
2792965006,
|
|
2073
|
+
1236535329,
|
|
2074
|
+
4129170786,
|
|
2075
|
+
3225465664,
|
|
2076
|
+
643717713,
|
|
2077
|
+
3921069994,
|
|
2078
|
+
3593408605,
|
|
2079
|
+
38016083,
|
|
2080
|
+
3634488961,
|
|
2081
|
+
3889429448,
|
|
2082
|
+
568446438,
|
|
2083
|
+
3275163606,
|
|
2084
|
+
4107603335,
|
|
2085
|
+
1163531501,
|
|
2086
|
+
2850285829,
|
|
2087
|
+
4243563512,
|
|
2088
|
+
1735328473,
|
|
2089
|
+
2368359562,
|
|
2090
|
+
4294588738,
|
|
2091
|
+
2272392833,
|
|
2092
|
+
1839030562,
|
|
2093
|
+
4259657740,
|
|
2094
|
+
2763975236,
|
|
2095
|
+
1272893353,
|
|
2096
|
+
4139469664,
|
|
2097
|
+
3200236656,
|
|
2098
|
+
681279174,
|
|
2099
|
+
3936430074,
|
|
2100
|
+
3572445317,
|
|
2101
|
+
76029189,
|
|
2102
|
+
3654602809,
|
|
2103
|
+
3873151461,
|
|
2104
|
+
530742520,
|
|
2105
|
+
3299628645,
|
|
2106
|
+
4096336452,
|
|
2107
|
+
1126891415,
|
|
2108
|
+
2878612391,
|
|
2109
|
+
4237533241,
|
|
2110
|
+
1700485571,
|
|
2111
|
+
2399980690,
|
|
2112
|
+
4293915773,
|
|
2113
|
+
2240044497,
|
|
2114
|
+
1873313359,
|
|
2115
|
+
4264355552,
|
|
2116
|
+
2734768916,
|
|
2117
|
+
1309151649,
|
|
2118
|
+
4149444226,
|
|
2119
|
+
3174756917,
|
|
2120
|
+
718787259,
|
|
2121
|
+
3951481745
|
|
2122
|
+
]);
|
|
2123
|
+
/**
|
|
2124
|
+
* Encodes text as UTF-8 bytes with RFC 1321 padding applied: a `0x80`
|
|
2125
|
+
* terminator, zero fill to 56 bytes mod 64, then the bit length as a
|
|
2126
|
+
* little-endian 64-bit integer.
|
|
2127
|
+
*/
|
|
2128
|
+
function paddedUtf8(text) {
|
|
2129
|
+
const bytes = new TextEncoder().encode(text);
|
|
2130
|
+
const paddedLength = (Math.floor((bytes.length + 8) / 64) + 1) * 64;
|
|
2131
|
+
const padded = new Uint8Array(paddedLength);
|
|
2132
|
+
padded.set(bytes);
|
|
2133
|
+
padded[bytes.length] = 128;
|
|
2134
|
+
const bitLength = bytes.length * 8;
|
|
2135
|
+
padded[paddedLength - 8] = bitLength & 255;
|
|
2136
|
+
padded[paddedLength - 7] = bitLength >>> 8 & 255;
|
|
2137
|
+
padded[paddedLength - 6] = bitLength >>> 16 & 255;
|
|
2138
|
+
padded[paddedLength - 5] = bitLength >>> 24 & 255;
|
|
2139
|
+
return padded;
|
|
2140
|
+
}
|
|
2141
|
+
/**
|
|
2142
|
+
* Adds two 32-bit values with wraparound, keeping intermediates inside the
|
|
2143
|
+
* 32-bit range JavaScript bitwise operators preserve. `Math.trunc` is not
|
|
2144
|
+
* a substitute here: the bitwise coercion is what performs the modular
|
|
2145
|
+
* wrap the algorithm requires.
|
|
2146
|
+
*/
|
|
2147
|
+
function add32(a, b) {
|
|
2148
|
+
return a + b | 0;
|
|
2149
|
+
}
|
|
2150
|
+
/**
|
|
2151
|
+
* Computes the MD5 digest of the text (as UTF-8) and returns it as 32
|
|
2152
|
+
* uppercase hexadecimal characters.
|
|
2153
|
+
*
|
|
2154
|
+
* Uppercase matches the identifier alphabet Xcode uses, which is what the
|
|
2155
|
+
* digest feeds (see `uuid.ts`).
|
|
2156
|
+
*/
|
|
2157
|
+
function md5Hex(text) {
|
|
2158
|
+
const message = paddedUtf8(text);
|
|
2159
|
+
let a0 = 1732584193;
|
|
2160
|
+
let b0 = 4023233417;
|
|
2161
|
+
let c0 = 2562383102;
|
|
2162
|
+
let d0 = 271733878;
|
|
2163
|
+
const words = /* @__PURE__ */ new Uint32Array(16);
|
|
2164
|
+
for (let offset = 0; offset < message.length; offset += 64) {
|
|
2165
|
+
for (let i = 0; i < 16; i++) {
|
|
2166
|
+
const base = offset + i * 4;
|
|
2167
|
+
words[i] = message[base] | message[base + 1] << 8 | message[base + 2] << 16 | message[base + 3] << 24;
|
|
2168
|
+
}
|
|
2169
|
+
let a = a0;
|
|
2170
|
+
let b = b0;
|
|
2171
|
+
let c = c0;
|
|
2172
|
+
let d = d0;
|
|
2173
|
+
for (let step = 0; step < 64; step++) {
|
|
2174
|
+
let mix;
|
|
2175
|
+
let wordIndex;
|
|
2176
|
+
if (step < 16) {
|
|
2177
|
+
mix = b & c | ~b & d;
|
|
2178
|
+
wordIndex = step;
|
|
2179
|
+
} else if (step < 32) {
|
|
2180
|
+
mix = d & b | ~d & c;
|
|
2181
|
+
wordIndex = (5 * step + 1) % 16;
|
|
2182
|
+
} else if (step < 48) {
|
|
2183
|
+
mix = b ^ c ^ d;
|
|
2184
|
+
wordIndex = (3 * step + 5) % 16;
|
|
2185
|
+
} else {
|
|
2186
|
+
mix = c ^ (b | ~d);
|
|
2187
|
+
wordIndex = 7 * step % 16;
|
|
2188
|
+
}
|
|
2189
|
+
const rotated = add32(add32(a, mix), add32(SINES[step], words[wordIndex]));
|
|
2190
|
+
const shift = SHIFTS[step];
|
|
2191
|
+
const next = add32(b, rotated << shift | rotated >>> 32 - shift);
|
|
2192
|
+
a = d;
|
|
2193
|
+
d = c;
|
|
2194
|
+
c = b;
|
|
2195
|
+
b = next;
|
|
2196
|
+
}
|
|
2197
|
+
a0 = add32(a0, a);
|
|
2198
|
+
b0 = add32(b0, b);
|
|
2199
|
+
c0 = add32(c0, c);
|
|
2200
|
+
d0 = add32(d0, d);
|
|
2201
|
+
}
|
|
2202
|
+
let hex = "";
|
|
2203
|
+
for (const word of [
|
|
2204
|
+
a0,
|
|
2205
|
+
b0,
|
|
2206
|
+
c0,
|
|
2207
|
+
d0
|
|
2208
|
+
]) for (let shift = 0; shift < 32; shift += 8) hex += (word >>> shift & 255).toString(16).toUpperCase().padStart(2, "0");
|
|
2209
|
+
return hex;
|
|
2210
|
+
}
|
|
2211
|
+
//#endregion
|
|
2212
|
+
//#region src/uuid.ts
|
|
2213
|
+
/**
|
|
2214
|
+
* Deterministic object identifiers for generated pbxproj objects.
|
|
2215
|
+
*
|
|
2216
|
+
* Xcode identifies every object with 24 hexadecimal characters. Generated
|
|
2217
|
+
* ids here are deterministic: the same seed always produces the same id, so
|
|
2218
|
+
* programmatic edits are reproducible and diffs stay minimal across runs.
|
|
2219
|
+
* The format is `XX` + the first 20 characters of `md5(seed)` + `XX`, which
|
|
2220
|
+
* is a valid identifier that remains recognizable as generated.
|
|
2221
|
+
*
|
|
2222
|
+
* @module
|
|
2223
|
+
*/
|
|
2224
|
+
/**
|
|
2225
|
+
* Formats the deterministic id for a seed without collision handling.
|
|
2226
|
+
*/
|
|
2227
|
+
function idForSeed(seed) {
|
|
2228
|
+
return `XX${md5Hex(seed).slice(0, 20)}XX`;
|
|
2229
|
+
}
|
|
2230
|
+
/**
|
|
2231
|
+
* Generates a deterministic 24-character object id from a seed string.
|
|
2232
|
+
*
|
|
2233
|
+
* When the id already exists in `existing`, the seed is retried with a
|
|
2234
|
+
* space appended until it is free, so ids stay deterministic (the same
|
|
2235
|
+
* seeds in the same order produce the same ids) while never colliding
|
|
2236
|
+
* within a document.
|
|
2237
|
+
*
|
|
2238
|
+
* @param seed Any text that identifies the object being created, for
|
|
2239
|
+
* example `PBXNativeTarget DemoWidget`.
|
|
2240
|
+
* @param existing Ids already present in the document.
|
|
2241
|
+
* @returns An id not contained in `existing`; the caller records it.
|
|
2242
|
+
*/
|
|
2243
|
+
function generateObjectId(seed, existing) {
|
|
2244
|
+
let currentSeed = seed;
|
|
2245
|
+
for (;;) {
|
|
2246
|
+
const id = idForSeed(currentSeed);
|
|
2247
|
+
if (!existing.has(id)) return id;
|
|
2248
|
+
currentSeed += " ";
|
|
2249
|
+
}
|
|
2250
|
+
}
|
|
2251
|
+
//#endregion
|
|
2252
|
+
//#region src/model/doctor.ts
|
|
2253
|
+
/**
|
|
2254
|
+
* Structural validation and cleanup for project documents.
|
|
2255
|
+
*
|
|
2256
|
+
* Real projects accumulate damage. References outlive the objects they
|
|
2257
|
+
* point at, orphans pile up, and entries lose their kind. Xcode tolerates
|
|
2258
|
+
* some of that silently and breaks on the rest. {@link validateProject}
|
|
2259
|
+
* reports the problems, and {@link pruneOrphanObjects} removes the
|
|
2260
|
+
* unreachable objects.
|
|
2261
|
+
*
|
|
2262
|
+
* @module
|
|
2263
|
+
*/
|
|
2264
|
+
/**
|
|
2265
|
+
* Object properties that hold a single reference to another object of the
|
|
2266
|
+
* same document. `remoteGlobalIDString` and `TestTargetID` are absent by
|
|
2267
|
+
* design. They reference objects of another container, so they cannot be
|
|
2268
|
+
* resolved here.
|
|
2269
|
+
*/
|
|
2270
|
+
const SCALAR_REFERENCE_PROPERTIES = [
|
|
2271
|
+
"baseConfigurationReference",
|
|
2272
|
+
"buildConfigurationList",
|
|
2273
|
+
"buildPhase",
|
|
2274
|
+
"containerPortal",
|
|
2275
|
+
"currentVersion",
|
|
2276
|
+
"fileRef",
|
|
2277
|
+
"mainGroup",
|
|
2278
|
+
"package",
|
|
2279
|
+
"productRef",
|
|
2280
|
+
"productRefGroup",
|
|
2281
|
+
"productReference",
|
|
2282
|
+
"remoteRef",
|
|
2283
|
+
"target",
|
|
2284
|
+
"targetProxy"
|
|
2285
|
+
];
|
|
2286
|
+
/**
|
|
2287
|
+
* Object properties that hold a list of references to other objects of the
|
|
2288
|
+
* same document.
|
|
2289
|
+
*/
|
|
2290
|
+
const LIST_REFERENCE_PROPERTIES = [
|
|
2291
|
+
"buildConfigurations",
|
|
2292
|
+
"buildPhases",
|
|
2293
|
+
"buildRules",
|
|
2294
|
+
"children",
|
|
2295
|
+
"dependencies",
|
|
2296
|
+
"exceptions",
|
|
2297
|
+
"files",
|
|
2298
|
+
"fileSystemSynchronizedGroups",
|
|
2299
|
+
"packageProductDependencies",
|
|
2300
|
+
"packageReferences",
|
|
2301
|
+
"targets"
|
|
2302
|
+
];
|
|
2303
|
+
/**
|
|
2304
|
+
* Collects the ids of every object reachable from the document's root
|
|
2305
|
+
* object.
|
|
2306
|
+
*
|
|
2307
|
+
* Reachability follows every string that names an existing object,
|
|
2308
|
+
* anywhere in an object's properties (including nested dictionaries, their
|
|
2309
|
+
* keys, and arrays). This is deliberately broader than the known reference
|
|
2310
|
+
* schema. An unknown-but-real reference keeps its object alive, so pruning
|
|
2311
|
+
* stays conservative.
|
|
2312
|
+
*/
|
|
2313
|
+
function reachableObjectIds(project) {
|
|
2314
|
+
const reachable = /* @__PURE__ */ new Set();
|
|
2315
|
+
const rootId = project.document["rootObject"];
|
|
2316
|
+
if (typeof rootId !== "string" || project.propertiesOfOptional(rootId) == null) return reachable;
|
|
2317
|
+
const pending = [rootId];
|
|
2318
|
+
const visitValue = (value) => {
|
|
2319
|
+
if (typeof value === "string") {
|
|
2320
|
+
if (!reachable.has(value) && project.propertiesOfOptional(value) != null) pending.push(value);
|
|
2321
|
+
return;
|
|
2322
|
+
}
|
|
2323
|
+
if (Array.isArray(value)) {
|
|
2324
|
+
for (const item of value) visitValue(item);
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
const nested = asDictionary(value);
|
|
2328
|
+
if (nested != null) for (const key of Object.keys(nested)) {
|
|
2329
|
+
visitValue(key);
|
|
2330
|
+
visitValue(nested[key]);
|
|
2331
|
+
}
|
|
2332
|
+
};
|
|
2333
|
+
while (pending.length > 0) {
|
|
2334
|
+
const id = pending.pop();
|
|
2335
|
+
if (reachable.has(id)) continue;
|
|
2336
|
+
reachable.add(id);
|
|
2337
|
+
const properties = project.propertiesOfOptional(id);
|
|
2338
|
+
if (properties != null) for (const key of Object.keys(properties)) visitValue(properties[key]);
|
|
2339
|
+
}
|
|
2340
|
+
return reachable;
|
|
2341
|
+
}
|
|
2342
|
+
/**
|
|
2343
|
+
* Reports one dangling reference when the id does not resolve.
|
|
2344
|
+
*/
|
|
2345
|
+
function checkReference(project, issues, objectId, property, id) {
|
|
2346
|
+
if (project.propertiesOfOptional(id) == null) issues.push({
|
|
2347
|
+
kind: "dangling-reference",
|
|
2348
|
+
message: `${objectId}.${property} references ${id}, which does not exist in the document`,
|
|
2349
|
+
objectId
|
|
2350
|
+
});
|
|
2351
|
+
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Validates the document's object graph and returns every problem found.
|
|
2354
|
+
* An empty array means the graph is structurally sound. The checks cover
|
|
2355
|
+
* the root object, object kinds, the known reference schema, and
|
|
2356
|
+
* reachability. See {@link ProjectIssueKind} for the meanings.
|
|
2357
|
+
*/
|
|
2358
|
+
function validateProject(project) {
|
|
2359
|
+
const issues = [];
|
|
2360
|
+
const rootId = project.document["rootObject"];
|
|
2361
|
+
const rootProperties = typeof rootId === "string" ? project.propertiesOfOptional(rootId) : void 0;
|
|
2362
|
+
if (rootProperties == null || rootProperties["isa"] !== "PBXProject") issues.push({
|
|
2363
|
+
kind: "dangling-root",
|
|
2364
|
+
message: typeof rootId === "string" ? `rootObject references ${rootId}, which is not a PBXProject in the document` : "The document has no rootObject"
|
|
2365
|
+
});
|
|
2366
|
+
for (const [id, view] of project.objects()) {
|
|
2367
|
+
const properties = view.properties;
|
|
2368
|
+
if (view.isa === "") issues.push({
|
|
2369
|
+
kind: "missing-isa",
|
|
2370
|
+
message: `${id} has no isa, so Xcode cannot read it`,
|
|
2371
|
+
objectId: id
|
|
2372
|
+
});
|
|
2373
|
+
for (const property of SCALAR_REFERENCE_PROPERTIES) {
|
|
2374
|
+
const value = properties[property];
|
|
2375
|
+
if (typeof value === "string") checkReference(project, issues, id, property, value);
|
|
2376
|
+
}
|
|
2377
|
+
for (const property of LIST_REFERENCE_PROPERTIES) {
|
|
2378
|
+
const value = properties[property];
|
|
2379
|
+
const items = Array.isArray(value) ? value : [];
|
|
2380
|
+
for (const item of items) if (typeof item === "string") checkReference(project, issues, id, property, item);
|
|
2381
|
+
}
|
|
2382
|
+
}
|
|
2383
|
+
const reachable = reachableObjectIds(project);
|
|
2384
|
+
if (reachable.size > 0) {
|
|
2385
|
+
for (const [id] of project.objects()) if (!reachable.has(id)) issues.push({
|
|
2386
|
+
kind: "unreachable-object",
|
|
2387
|
+
message: `${id} is unreachable from the root object`,
|
|
2388
|
+
objectId: id
|
|
2389
|
+
});
|
|
2390
|
+
}
|
|
2391
|
+
return issues;
|
|
2392
|
+
}
|
|
2393
|
+
/**
|
|
2394
|
+
* Removes every object unreachable from the document's root object and
|
|
2395
|
+
* returns the removed ids, in document order. A document whose root is
|
|
2396
|
+
* missing prunes nothing, since reachability is undefined there.
|
|
2397
|
+
*
|
|
2398
|
+
* Reachability is the conservative walk of {@link reachableObjectIds}.
|
|
2399
|
+
* Any real reference keeps an object alive, even when the reference
|
|
2400
|
+
* property is outside the known schema.
|
|
2401
|
+
*/
|
|
2402
|
+
function pruneOrphanObjects(project) {
|
|
2403
|
+
const reachable = reachableObjectIds(project);
|
|
2404
|
+
if (reachable.size === 0) return [];
|
|
2405
|
+
const orphanIds = [];
|
|
2406
|
+
for (const [id] of project.objects()) if (!reachable.has(id)) orphanIds.push(id);
|
|
2407
|
+
for (const id of orphanIds) project.removeObject(id);
|
|
2408
|
+
return orphanIds;
|
|
2409
|
+
}
|
|
2410
|
+
//#endregion
|
|
2411
|
+
//#region src/model/settings.ts
|
|
2412
|
+
/**
|
|
2413
|
+
* Build-configuration helpers shared by targets and the root project
|
|
2414
|
+
* object, both of which own an `XCConfigurationList`.
|
|
2415
|
+
*
|
|
2416
|
+
* @module
|
|
2417
|
+
*/
|
|
2418
|
+
/**
|
|
2419
|
+
* The views of a configuration list's build configurations, in list order.
|
|
2420
|
+
* Dangling ids and non-dictionary entries of malformed documents are
|
|
2421
|
+
* skipped.
|
|
2422
|
+
*/
|
|
2423
|
+
function configurationsOf(project, configurationListId) {
|
|
2424
|
+
const list = asDictionary(project.propertiesOfOptional(configurationListId));
|
|
2425
|
+
const configurations = [];
|
|
2426
|
+
for (const id of stringItems(list?.["buildConfigurations"])) {
|
|
2427
|
+
const configuration = project.get(id);
|
|
2428
|
+
if (configuration != null) configurations.push(configuration);
|
|
2429
|
+
}
|
|
2430
|
+
return configurations;
|
|
2431
|
+
}
|
|
2432
|
+
/**
|
|
2433
|
+
* The settings dictionary of a configuration list's default configuration:
|
|
2434
|
+
* the one named by `defaultConfigurationName`, falling back to the first
|
|
2435
|
+
* configuration. Returns `undefined` when the list has no configurations
|
|
2436
|
+
* or the default carries no settings dictionary.
|
|
2437
|
+
*/
|
|
2438
|
+
function defaultConfigurationSettingsOf(project, configurationListId) {
|
|
2439
|
+
const list = asDictionary(project.propertiesOfOptional(configurationListId));
|
|
2440
|
+
const configurations = configurationsOf(project, configurationListId);
|
|
2441
|
+
const defaultName = asString(list?.["defaultConfigurationName"]);
|
|
2442
|
+
return asDictionary((configurations.find((configuration) => configuration.getString("name") === defaultName) ?? configurations[0])?.properties["buildSettings"]);
|
|
2443
|
+
}
|
|
2444
|
+
//#endregion
|
|
2445
|
+
//#region src/model/target.ts
|
|
2446
|
+
/**
|
|
2447
|
+
* Views of the project's targets. The behavior all target kinds share
|
|
2448
|
+
* lives on the {@link Target} base class, and {@link NativeTarget}
|
|
2449
|
+
* extends it with products, embedding, synchronized folders, Swift
|
|
2450
|
+
* packages, and system frameworks.
|
|
2451
|
+
*
|
|
2452
|
+
* Reads are deliberately soft: user-generated projects can be malformed,
|
|
2453
|
+
* so lookups return `undefined` instead of throwing wherever a document
|
|
2454
|
+
* could legally or illegally omit something. Mutations create any missing
|
|
2455
|
+
* structure they need.
|
|
2456
|
+
*
|
|
2457
|
+
* @module
|
|
2458
|
+
*/
|
|
2459
|
+
/**
|
|
2460
|
+
* The behavior every target kind shares. A target of any kind carries
|
|
2461
|
+
* build configurations and settings, build phases, and dependencies, and
|
|
2462
|
+
* this class holds their accessors and mutations. `PBXNativeTarget`,
|
|
2463
|
+
* `PBXAggregateTarget`, and `PBXLegacyTarget` all extend it, so code that
|
|
2464
|
+
* walks or rewires targets can accept any of them.
|
|
2465
|
+
*/
|
|
2466
|
+
var Target = class extends XcodeObject {
|
|
2467
|
+
/**
|
|
2468
|
+
* The target's name, when present.
|
|
2469
|
+
*/
|
|
2470
|
+
get name() {
|
|
2471
|
+
return this.getString("name");
|
|
2472
|
+
}
|
|
2473
|
+
/**
|
|
2474
|
+
* The views of the target's build configurations, in list order.
|
|
2475
|
+
*/
|
|
2476
|
+
buildConfigurations() {
|
|
2477
|
+
return configurationsOf(this.project, this.getString("buildConfigurationList"));
|
|
2478
|
+
}
|
|
2479
|
+
/**
|
|
2480
|
+
* The settings dictionary of the target's default configuration: the one
|
|
2481
|
+
* named by the list's `defaultConfigurationName`, falling back to the
|
|
2482
|
+
* first configuration. Returns `undefined` when the target has no
|
|
2483
|
+
* configurations or the default carries no settings dictionary.
|
|
2484
|
+
*/
|
|
2485
|
+
defaultConfigurationSettings() {
|
|
2486
|
+
return defaultConfigurationSettingsOf(this.project, this.getString("buildConfigurationList"));
|
|
2487
|
+
}
|
|
2488
|
+
/**
|
|
2489
|
+
* Reads a build setting from the target's default configuration,
|
|
2490
|
+
* inheriting from the project-level configuration when the target omits
|
|
2491
|
+
* the key. This mirrors how Xcode resolves settings hierarchically;
|
|
2492
|
+
* generated app templates set values like `SDKROOT` only at the project
|
|
2493
|
+
* level.
|
|
2494
|
+
*
|
|
2495
|
+
* Only string values are returned; a list- or number-valued setting reads
|
|
2496
|
+
* as `undefined`.
|
|
2497
|
+
*/
|
|
2498
|
+
getBuildSetting(key) {
|
|
2499
|
+
const targetSettings = this.defaultConfigurationSettings();
|
|
2500
|
+
if (targetSettings != null && key in targetSettings) return asString(targetSettings[key]);
|
|
2501
|
+
return asString(this.project.rootProject.defaultConfigurationSettings()?.[key]);
|
|
2502
|
+
}
|
|
2503
|
+
/**
|
|
2504
|
+
* Writes a build setting on every configuration of the target, so Debug
|
|
2505
|
+
* and Release stay consistent.
|
|
2506
|
+
*/
|
|
2507
|
+
setBuildSetting(key, value) {
|
|
2508
|
+
for (const configuration of this.buildConfigurations()) {
|
|
2509
|
+
const settings = asDictionary(configuration.properties["buildSettings"]);
|
|
2510
|
+
if (settings == null) configuration.properties["buildSettings"] = { [key]: value };
|
|
2511
|
+
else settings[key] = value;
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
/**
|
|
2515
|
+
* Removes a build setting from every configuration of the target.
|
|
2516
|
+
*/
|
|
2517
|
+
removeBuildSetting(key) {
|
|
2518
|
+
for (const configuration of this.buildConfigurations()) {
|
|
2519
|
+
const settings = asDictionary(configuration.properties["buildSettings"]);
|
|
2520
|
+
if (settings != null) delete settings[key];
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2523
|
+
/**
|
|
2524
|
+
* The views of the target's dependency objects
|
|
2525
|
+
* (`PBXTargetDependency`), in declaration order. Resolve a dependency's
|
|
2526
|
+
* target through its `target` property.
|
|
2527
|
+
*/
|
|
2528
|
+
dependencies() {
|
|
2529
|
+
return this.referencedViews("dependencies");
|
|
2530
|
+
}
|
|
2531
|
+
/**
|
|
2532
|
+
* The views of the target's build phases, in build order.
|
|
2533
|
+
*/
|
|
2534
|
+
buildPhases() {
|
|
2535
|
+
const phases = [];
|
|
2536
|
+
for (const id of stringItems(this.properties["buildPhases"])) {
|
|
2537
|
+
const phase = this.project.get(id);
|
|
2538
|
+
if (phase instanceof BuildPhase) phases.push(phase);
|
|
2539
|
+
}
|
|
2540
|
+
return phases;
|
|
2541
|
+
}
|
|
2542
|
+
/**
|
|
2543
|
+
* Finds the target's first build phase with the given isa, and, when
|
|
2544
|
+
* `name` is provided, the given display name.
|
|
2545
|
+
*/
|
|
2546
|
+
findBuildPhase(isa, name) {
|
|
2547
|
+
return this.buildPhases().find((phase) => phase.isa === isa && (name == null || phase.name === name));
|
|
2548
|
+
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Returns the target's build phase with the given isa and properties,
|
|
2551
|
+
* creating and appending it when missing. The properties apply only on
|
|
2552
|
+
* creation; an existing phase is returned as is.
|
|
2553
|
+
*
|
|
2554
|
+
* The match key is the isa plus the `name` property when one is given,
|
|
2555
|
+
* so differently named copy-files phases coexist.
|
|
2556
|
+
*/
|
|
2557
|
+
ensureBuildPhase(isa, properties = {}) {
|
|
2558
|
+
const name = asString(properties["name"]);
|
|
2559
|
+
const existing = this.findBuildPhase(isa, name);
|
|
2560
|
+
if (existing != null) return existing;
|
|
2561
|
+
const phase = this.project.add(isa, {
|
|
2562
|
+
isa,
|
|
2563
|
+
buildActionMask: 2147483647,
|
|
2564
|
+
files: [],
|
|
2565
|
+
runOnlyForDeploymentPostprocessing: 0,
|
|
2566
|
+
...properties
|
|
2567
|
+
}, `${isa} ${this.id} ${name ?? ""}`);
|
|
2568
|
+
ensureArray(this.properties, "buildPhases").push(phase.id);
|
|
2569
|
+
return phase;
|
|
2570
|
+
}
|
|
2571
|
+
/**
|
|
2572
|
+
* The target's shell-script phase with the given name, created with the
|
|
2573
|
+
* usual defaults (`/bin/sh`, empty input and output lists) when missing.
|
|
2574
|
+
* The script and other properties apply only on creation.
|
|
2575
|
+
*
|
|
2576
|
+
* @param name Display name of the phase, which is also its match key.
|
|
2577
|
+
* @param properties Phase properties, most usefully `shellScript`.
|
|
2578
|
+
*/
|
|
2579
|
+
ensureShellScriptPhase(name, properties = {}) {
|
|
2580
|
+
return this.ensureBuildPhase("PBXShellScriptBuildPhase", {
|
|
2581
|
+
inputFileListPaths: [],
|
|
2582
|
+
inputPaths: [],
|
|
2583
|
+
name,
|
|
2584
|
+
outputFileListPaths: [],
|
|
2585
|
+
outputPaths: [],
|
|
2586
|
+
shellPath: "/bin/sh",
|
|
2587
|
+
shellScript: "",
|
|
2588
|
+
...properties
|
|
2589
|
+
});
|
|
2590
|
+
}
|
|
2591
|
+
/**
|
|
2592
|
+
* Adds a dependency on another target of the same project, wiring the
|
|
2593
|
+
* `PBXContainerItemProxy` and `PBXTargetDependency` pair Xcode uses to
|
|
2594
|
+
* express it. Adding an existing dependency is a no-op.
|
|
2595
|
+
*
|
|
2596
|
+
* @returns The view of the target dependency object.
|
|
2597
|
+
*/
|
|
2598
|
+
addDependency(dependency) {
|
|
2599
|
+
for (const id of stringItems(this.properties["dependencies"])) {
|
|
2600
|
+
const existing = this.project.get(id);
|
|
2601
|
+
if (existing?.getString("target") === dependency.id) return existing;
|
|
2602
|
+
}
|
|
2603
|
+
const proxy = this.project.add(Isa.containerItemProxy, {
|
|
2604
|
+
containerPortal: this.project.rootProject.id,
|
|
2605
|
+
proxyType: 1,
|
|
2606
|
+
remoteGlobalIDString: dependency.id,
|
|
2607
|
+
remoteInfo: dependency.name ?? dependency.id
|
|
2608
|
+
}, `${Isa.containerItemProxy} ${this.id} ${dependency.id}`);
|
|
2609
|
+
const targetDependency = this.project.add(Isa.targetDependency, {
|
|
2610
|
+
target: dependency.id,
|
|
2611
|
+
targetProxy: proxy.id
|
|
2612
|
+
}, `${Isa.targetDependency} ${this.id} ${dependency.id}`);
|
|
2613
|
+
ensureArray(this.properties, "dependencies").push(targetDependency.id);
|
|
2614
|
+
return targetDependency;
|
|
2615
|
+
}
|
|
2616
|
+
};
|
|
2617
|
+
/**
|
|
2618
|
+
* A `PBXNativeTarget` is a product the project compiles and packages
|
|
2619
|
+
* itself, such as an application or an app extension.
|
|
2620
|
+
*/
|
|
2621
|
+
var NativeTarget = class extends Target {
|
|
2622
|
+
/**
|
|
2623
|
+
* The target's product type identifier, for example
|
|
2624
|
+
* `com.apple.product-type.application`.
|
|
2625
|
+
*/
|
|
2626
|
+
get productType() {
|
|
2627
|
+
return this.getString("productType");
|
|
2628
|
+
}
|
|
2629
|
+
/**
|
|
2630
|
+
* Rewrites the target's product type. Used by packaging repairs that
|
|
2631
|
+
* convert foundation extensions into ExtensionKit extensions.
|
|
2632
|
+
*/
|
|
2633
|
+
set productType(value) {
|
|
2634
|
+
this.properties["productType"] = value;
|
|
2635
|
+
}
|
|
2636
|
+
/**
|
|
2637
|
+
* The view of the target's product file reference, when the target has
|
|
2638
|
+
* one.
|
|
2639
|
+
*/
|
|
2640
|
+
get productReference() {
|
|
2641
|
+
const id = this.getString("productReference");
|
|
2642
|
+
return id == null ? void 0 : this.project.get(id);
|
|
2643
|
+
}
|
|
2644
|
+
/**
|
|
2645
|
+
* Whether the target builds for watchOS, decided by its product type or
|
|
2646
|
+
* its watchOS deployment-target setting.
|
|
2647
|
+
*/
|
|
2648
|
+
isWatchOS() {
|
|
2649
|
+
if (this.productType === ProductType.watchApp) return true;
|
|
2650
|
+
const defaultSettings = this.defaultConfigurationSettings();
|
|
2651
|
+
return defaultSettings != null && "WATCHOS_DEPLOYMENT_TARGET" in defaultSettings;
|
|
2652
|
+
}
|
|
2653
|
+
/**
|
|
2654
|
+
* The views of the target's Swift package product dependencies, in
|
|
2655
|
+
* declaration order.
|
|
2656
|
+
*/
|
|
2657
|
+
packageProductDependencies() {
|
|
2658
|
+
return this.referencedViews("packageProductDependencies");
|
|
2659
|
+
}
|
|
2660
|
+
/**
|
|
2661
|
+
* The views of the target's file-system-synchronized folders, in
|
|
2662
|
+
* declaration order.
|
|
2663
|
+
*/
|
|
2664
|
+
syncGroups() {
|
|
2665
|
+
return this.referencedViews("fileSystemSynchronizedGroups").filter((view) => view instanceof SyncRootGroup);
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* The views of the target's build rules, in evaluation order. Empty for
|
|
2669
|
+
* targets without custom rules, which is nearly all of them.
|
|
2670
|
+
*/
|
|
2671
|
+
buildRules() {
|
|
2672
|
+
return this.referencedViews("buildRules").filter((view) => view instanceof BuildRule);
|
|
2673
|
+
}
|
|
2674
|
+
/**
|
|
2675
|
+
* The target's sources phase, created when missing.
|
|
2676
|
+
*/
|
|
2677
|
+
ensureSourcesPhase() {
|
|
2678
|
+
return this.ensureBuildPhase(Isa.sourcesBuildPhase);
|
|
2679
|
+
}
|
|
2680
|
+
/**
|
|
2681
|
+
* The target's frameworks phase, created when missing.
|
|
2682
|
+
*/
|
|
2683
|
+
ensureFrameworksPhase() {
|
|
2684
|
+
return this.ensureBuildPhase(Isa.frameworksBuildPhase);
|
|
2685
|
+
}
|
|
2686
|
+
/**
|
|
2687
|
+
* The target's resources phase, created when missing.
|
|
2688
|
+
*/
|
|
2689
|
+
ensureResourcesPhase() {
|
|
2690
|
+
return this.ensureBuildPhase(Isa.resourcesBuildPhase);
|
|
2691
|
+
}
|
|
2692
|
+
/**
|
|
2693
|
+
* Embeds another target's product into this target through the
|
|
2694
|
+
* copy-files phase its product type calls for ("Embed Foundation
|
|
2695
|
+
* Extensions", "Embed App Clips", "Embed Watch Content", or "Embed
|
|
2696
|
+
* ExtensionKit Extensions").
|
|
2697
|
+
*
|
|
2698
|
+
* An existing phase with the same name is reused, its destination is
|
|
2699
|
+
* repaired to the type's canonical values, and the product's build file
|
|
2700
|
+
* is deduplicated, so embedding is idempotent.
|
|
2701
|
+
*
|
|
2702
|
+
* @returns The view of the embed phase, or `undefined` when the embedded
|
|
2703
|
+
* target has no product reference to embed.
|
|
2704
|
+
*/
|
|
2705
|
+
embed(extension) {
|
|
2706
|
+
const product = extension.productReference;
|
|
2707
|
+
if (product == null) return;
|
|
2708
|
+
const destination = embedDestinationFor(extension.isWatchOS() ? ProductType.watchApp : extension.productType);
|
|
2709
|
+
const phase = this.ensureBuildPhase(Isa.copyFilesBuildPhase, { name: destination.phaseName });
|
|
2710
|
+
phase.set("dstPath", destination.dstPath);
|
|
2711
|
+
phase.set("dstSubfolderSpec", destination.dstSubfolderSpec);
|
|
2712
|
+
phase.ensureBuildFile(product, { settings: { ATTRIBUTES: ["RemoveHeadersOnCopy"] } });
|
|
2713
|
+
return phase;
|
|
2714
|
+
}
|
|
2715
|
+
/**
|
|
2716
|
+
* The on-disk folder paths of the file-system-synchronized groups linked
|
|
2717
|
+
* to this target. Empty for targets without synchronized folders
|
|
2718
|
+
* (projects predating Xcode 16).
|
|
2719
|
+
*/
|
|
2720
|
+
syncGroupPaths() {
|
|
2721
|
+
const paths = [];
|
|
2722
|
+
for (const id of stringItems(this.properties["fileSystemSynchronizedGroups"])) {
|
|
2723
|
+
const group = this.project.get(id);
|
|
2724
|
+
const path = group instanceof SyncRootGroup ? group.path : void 0;
|
|
2725
|
+
if (path != null) paths.push(path);
|
|
2726
|
+
}
|
|
2727
|
+
return paths;
|
|
2728
|
+
}
|
|
2729
|
+
/**
|
|
2730
|
+
* Creates a file-system-synchronized folder for an on-disk path, links it
|
|
2731
|
+
* to this target, and registers it in the project's main group so Xcode
|
|
2732
|
+
* shows it in the navigator. When the target already links a folder with
|
|
2733
|
+
* the same path, that folder is returned instead, so re-running a
|
|
2734
|
+
* scaffold step cannot duplicate groups.
|
|
2735
|
+
*
|
|
2736
|
+
* @param path Folder path relative to the project root, for example the
|
|
2737
|
+
* target's name.
|
|
2738
|
+
* @returns The view of the target's synchronized folder for the path.
|
|
2739
|
+
*/
|
|
2740
|
+
addSyncGroup(path) {
|
|
2741
|
+
for (const id of stringItems(this.properties["fileSystemSynchronizedGroups"])) {
|
|
2742
|
+
const existing = this.project.get(id);
|
|
2743
|
+
if (existing instanceof SyncRootGroup && existing.path === path) return existing;
|
|
2744
|
+
}
|
|
2745
|
+
const group = this.project.add(Isa.fileSystemSynchronizedRootGroup, {
|
|
2746
|
+
path,
|
|
2747
|
+
sourceTree: "<group>"
|
|
2748
|
+
}, `${Isa.fileSystemSynchronizedRootGroup} ${path}`);
|
|
2749
|
+
ensureArray(this.properties, "fileSystemSynchronizedGroups").push(group.id);
|
|
2750
|
+
this.project.rootProject.mainGroup()?.addChild(group);
|
|
2751
|
+
return group;
|
|
2752
|
+
}
|
|
2753
|
+
/**
|
|
2754
|
+
* Links a Swift package product to this target. It creates the
|
|
2755
|
+
* `XCSwiftPackageProductDependency`, registers it on the target, and
|
|
2756
|
+
* ensures the frameworks phase carries its build file. Linking an
|
|
2757
|
+
* already linked product is a no-op.
|
|
2758
|
+
*
|
|
2759
|
+
* @param options.productName The product to link, as the package
|
|
2760
|
+
* manifest names it.
|
|
2761
|
+
* @param options.packageReference The package reference view returned by
|
|
2762
|
+
* {@link XcodeProject.addSwiftPackage} or
|
|
2763
|
+
* {@link XcodeProject.findSwiftPackage}.
|
|
2764
|
+
* @returns The view of the product dependency.
|
|
2765
|
+
*/
|
|
2766
|
+
addSwiftPackageProduct(options) {
|
|
2767
|
+
for (const id of stringItems(this.properties["packageProductDependencies"])) {
|
|
2768
|
+
const existing = this.project.get(id);
|
|
2769
|
+
if (existing?.getString("productName") === options.productName && existing.getString("package") === options.packageReference.id) return existing;
|
|
2770
|
+
}
|
|
2771
|
+
const productDependency = this.project.add(Isa.swiftPackageProductDependency, {
|
|
2772
|
+
package: options.packageReference.id,
|
|
2773
|
+
productName: options.productName
|
|
2774
|
+
}, `${Isa.swiftPackageProductDependency} ${this.id} ${options.productName}`);
|
|
2775
|
+
ensureArray(this.properties, "packageProductDependencies").push(productDependency.id);
|
|
2776
|
+
this.ensureFrameworksPhase().ensureBuildFile(productDependency, { referenceKey: "productRef" });
|
|
2777
|
+
return productDependency;
|
|
2778
|
+
}
|
|
2779
|
+
/**
|
|
2780
|
+
* Links a system framework (for example `Messages`) to this target. It
|
|
2781
|
+
* reuses or creates the file reference under the SDK's frameworks
|
|
2782
|
+
* directory and makes sure the frameworks phase carries its build file.
|
|
2783
|
+
* Linking an already linked framework is a no-op.
|
|
2784
|
+
*
|
|
2785
|
+
* @param name Framework name without the `.framework` suffix.
|
|
2786
|
+
* @returns The view of the framework's file reference.
|
|
2787
|
+
*/
|
|
2788
|
+
addSystemFramework(name) {
|
|
2789
|
+
const path = `System/Library/Frameworks/${name}.framework`;
|
|
2790
|
+
let reference;
|
|
2791
|
+
for (const [, view] of this.project.objects()) if (view.isa === Isa.fileReference && view.getString("path") === path) {
|
|
2792
|
+
reference = view;
|
|
2793
|
+
break;
|
|
2794
|
+
}
|
|
2795
|
+
reference ??= this.project.add(Isa.fileReference, {
|
|
2796
|
+
lastKnownFileType: "wrapper.framework",
|
|
2797
|
+
name: `${name}.framework`,
|
|
2798
|
+
path,
|
|
2799
|
+
sourceTree: "SDKROOT"
|
|
2800
|
+
}, `${Isa.fileReference} ${path}`);
|
|
2801
|
+
this.ensureFrameworksPhase().ensureBuildFile(reference);
|
|
2802
|
+
return reference;
|
|
2803
|
+
}
|
|
2804
|
+
};
|
|
2805
|
+
/**
|
|
2806
|
+
* An aggregate target produces nothing itself. It exists to group other
|
|
2807
|
+
* targets through its dependencies and to run script or copy-files
|
|
2808
|
+
* phases, and the shared target surface covers everything it carries.
|
|
2809
|
+
*/
|
|
2810
|
+
var AggregateTarget = class extends Target {};
|
|
2811
|
+
/**
|
|
2812
|
+
* A legacy target shells out to an external build tool such as make
|
|
2813
|
+
* instead of using Xcode's build system.
|
|
2814
|
+
*/
|
|
2815
|
+
var LegacyTarget = class extends Target {
|
|
2816
|
+
/**
|
|
2817
|
+
* The build tool the target invokes, as an absolute path.
|
|
2818
|
+
*/
|
|
2819
|
+
get buildToolPath() {
|
|
2820
|
+
return this.getString("buildToolPath");
|
|
2821
|
+
}
|
|
2822
|
+
/**
|
|
2823
|
+
* The arguments passed to the build tool, as one shell-style string.
|
|
2824
|
+
*/
|
|
2825
|
+
get buildArgumentsString() {
|
|
2826
|
+
return this.getString("buildArgumentsString");
|
|
2827
|
+
}
|
|
2828
|
+
/**
|
|
2829
|
+
* The working directory the build tool runs in, when the target sets
|
|
2830
|
+
* one.
|
|
2831
|
+
*/
|
|
2832
|
+
get buildWorkingDirectory() {
|
|
2833
|
+
return this.getString("buildWorkingDirectory");
|
|
2834
|
+
}
|
|
2835
|
+
};
|
|
2836
|
+
//#endregion
|
|
2837
|
+
//#region src/model/project.ts
|
|
2838
|
+
/**
|
|
2839
|
+
* The project document model. It gives typed, mutable access to a parsed
|
|
2840
|
+
* `project.pbxproj`.
|
|
2841
|
+
*
|
|
2842
|
+
* The model is a set of lightweight views over the plain parsed document.
|
|
2843
|
+
* All state lives in the document itself; views hold only an id and a
|
|
2844
|
+
* project reference, so model mutations and direct dictionary writes
|
|
2845
|
+
* compose freely and {@link XcodeProject.build} always serializes the
|
|
2846
|
+
* current state. New objects receive deterministic identifiers (see
|
|
2847
|
+
* `uuid.ts`), so programmatic edits are reproducible run to run.
|
|
2848
|
+
*
|
|
2849
|
+
* @module
|
|
2850
|
+
*/
|
|
2851
|
+
/**
|
|
2852
|
+
* The `PBXProject` object at the document root. It owns the target list,
|
|
2853
|
+
* the main group, and the project-level configurations.
|
|
2854
|
+
*/
|
|
2855
|
+
var RootProject = class extends XcodeObject {
|
|
2856
|
+
/**
|
|
2857
|
+
* Ids of the project's targets, in project order.
|
|
2858
|
+
*/
|
|
2859
|
+
targetIds() {
|
|
2860
|
+
return stringItems(this.properties["targets"]);
|
|
2861
|
+
}
|
|
2862
|
+
/**
|
|
2863
|
+
* The view of the project's main group, when the document has one. The
|
|
2864
|
+
* main group is the root of Xcode's navigator tree.
|
|
2865
|
+
*/
|
|
2866
|
+
mainGroup() {
|
|
2867
|
+
const group = this.project.get(this.getString("mainGroup"));
|
|
2868
|
+
return group instanceof Group ? group : void 0;
|
|
2869
|
+
}
|
|
2870
|
+
/**
|
|
2871
|
+
* The settings dictionary of the project-level default configuration.
|
|
2872
|
+
* Targets inherit from these settings; see
|
|
2873
|
+
* {@link NativeTarget.getBuildSetting}.
|
|
2874
|
+
*/
|
|
2875
|
+
defaultConfigurationSettings() {
|
|
2876
|
+
return defaultConfigurationSettingsOf(this.project, this.getString("buildConfigurationList"));
|
|
2877
|
+
}
|
|
2878
|
+
/**
|
|
2879
|
+
* The views of the project's Swift package references, remote and local,
|
|
2880
|
+
* in declaration order.
|
|
2881
|
+
*/
|
|
2882
|
+
packageReferences() {
|
|
2883
|
+
return this.referencedViews("packageReferences");
|
|
2884
|
+
}
|
|
2885
|
+
/**
|
|
2886
|
+
* The group product references live in, creating it (and registering it
|
|
2887
|
+
* as the project's `productRefGroup`) when missing.
|
|
2888
|
+
*/
|
|
2889
|
+
ensureProductsGroup() {
|
|
2890
|
+
const existing = this.project.get(this.getString("productRefGroup"));
|
|
2891
|
+
if (existing instanceof Group) return existing;
|
|
2892
|
+
const group = this.project.add(Isa.group, {
|
|
2893
|
+
children: [],
|
|
2894
|
+
name: "Products",
|
|
2895
|
+
sourceTree: "<group>"
|
|
2896
|
+
}, `${Isa.group} Products`);
|
|
2897
|
+
this.mainGroup()?.addChild(group);
|
|
2898
|
+
this.set("productRefGroup", group.id);
|
|
2899
|
+
return group;
|
|
2900
|
+
}
|
|
2901
|
+
};
|
|
2902
|
+
/**
|
|
2903
|
+
* A parsed `project.pbxproj` with typed, mutable access to its objects.
|
|
2904
|
+
*
|
|
2905
|
+
* ```ts
|
|
2906
|
+
* const project = XcodeProject.parse(pbxprojText);
|
|
2907
|
+
* const app = project.findMainAppTarget("ios");
|
|
2908
|
+
* app?.setBuildSetting("MARKETING_VERSION", "1.2.0");
|
|
2909
|
+
* const text = project.build();
|
|
2910
|
+
* ```
|
|
2911
|
+
*/
|
|
2912
|
+
var XcodeProject = class XcodeProject {
|
|
2913
|
+
/** The parsed document this model wraps. */
|
|
2914
|
+
document;
|
|
2915
|
+
/** The document's `objects` dictionary. */
|
|
2916
|
+
objectsDictionary;
|
|
2917
|
+
/**
|
|
2918
|
+
* Identity map of object views, one view per id, created on first
|
|
2919
|
+
* access, so views of the same object compare with `===`.
|
|
2920
|
+
*/
|
|
2921
|
+
views = /* @__PURE__ */ new Map();
|
|
2922
|
+
constructor(document) {
|
|
2923
|
+
const objects = asDictionary(document["objects"]);
|
|
2924
|
+
if (objects == null) throw new XcodeModelError("The document has no objects dictionary");
|
|
2925
|
+
this.document = document;
|
|
2926
|
+
this.objectsDictionary = objects;
|
|
2927
|
+
}
|
|
2928
|
+
/**
|
|
2929
|
+
* Parses pbxproj text and wraps it in a model.
|
|
2930
|
+
*
|
|
2931
|
+
* @throws PbxprojParseError when the text is malformed.
|
|
2932
|
+
* @throws XcodeModelError when the document has no objects dictionary.
|
|
2933
|
+
*/
|
|
2934
|
+
static parse(text) {
|
|
2935
|
+
const document = asDictionary(parsePbxproj(text));
|
|
2936
|
+
if (document == null) throw new XcodeModelError("The document root is not a dictionary");
|
|
2937
|
+
return new XcodeProject(document);
|
|
2938
|
+
}
|
|
2939
|
+
/**
|
|
2940
|
+
* Wraps an already parsed document in a model. The document is used in
|
|
2941
|
+
* place, not copied; model mutations write into it.
|
|
2942
|
+
*/
|
|
2943
|
+
static fromDocument(document) {
|
|
2944
|
+
return new XcodeProject(document);
|
|
2945
|
+
}
|
|
2946
|
+
/**
|
|
2947
|
+
* Serializes the current document state to pbxproj text in Xcode's
|
|
2948
|
+
* canonical layout.
|
|
2949
|
+
*/
|
|
2950
|
+
build() {
|
|
2951
|
+
return buildPbxproj(this.document);
|
|
2952
|
+
}
|
|
2953
|
+
/**
|
|
2954
|
+
* The raw properties dictionary of an object.
|
|
2955
|
+
*
|
|
2956
|
+
* @throws XcodeModelError when no object with the id exists; views use
|
|
2957
|
+
* this accessor, so a view of a deleted object fails loudly instead of
|
|
2958
|
+
* resurrecting the entry.
|
|
2959
|
+
*/
|
|
2960
|
+
propertiesOf(id) {
|
|
2961
|
+
const properties = asDictionary(this.objectsDictionary[id]);
|
|
2962
|
+
if (properties == null) throw new XcodeModelError(`No object with id ${id} exists in the document`);
|
|
2963
|
+
return properties;
|
|
2964
|
+
}
|
|
2965
|
+
/**
|
|
2966
|
+
* The raw properties dictionary of an object, or `undefined` when the id
|
|
2967
|
+
* is absent, dangling, or not a dictionary.
|
|
2968
|
+
*/
|
|
2969
|
+
propertiesOfOptional(id) {
|
|
2970
|
+
return id == null ? void 0 : asDictionary(this.objectsDictionary[id]);
|
|
2971
|
+
}
|
|
2972
|
+
/**
|
|
2973
|
+
* The view of an object by id, or `undefined` when the id is absent or
|
|
2974
|
+
* its entry is not a dictionary. Repeated lookups return the same view
|
|
2975
|
+
* instance.
|
|
2976
|
+
*/
|
|
2977
|
+
get(id) {
|
|
2978
|
+
if (id == null) return;
|
|
2979
|
+
const existing = this.views.get(id);
|
|
2980
|
+
if (existing != null) return existing;
|
|
2981
|
+
const properties = asDictionary(this.objectsDictionary[id]);
|
|
2982
|
+
if (properties == null) return;
|
|
2983
|
+
const view = this.createView(id, asString(properties["isa"]) ?? "");
|
|
2984
|
+
this.views.set(id, view);
|
|
2985
|
+
return view;
|
|
2986
|
+
}
|
|
2987
|
+
/**
|
|
2988
|
+
* Iterates `[id, view]` over every well-formed object in the document,
|
|
2989
|
+
* in document order. Entries that are not dictionaries are skipped.
|
|
2990
|
+
*/
|
|
2991
|
+
*objects() {
|
|
2992
|
+
for (const id of Object.keys(this.objectsDictionary)) {
|
|
2993
|
+
const view = this.get(id);
|
|
2994
|
+
if (view != null) yield [id, view];
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
/**
|
|
2998
|
+
* Generates a deterministic 24-character id from a seed, avoiding every
|
|
2999
|
+
* id the document currently contains. The id is not reserved; adding an
|
|
3000
|
+
* object with it (see {@link add}) is what claims it.
|
|
3001
|
+
*/
|
|
3002
|
+
generateId(seed) {
|
|
3003
|
+
return generateObjectId(seed, new Set(Object.keys(this.objectsDictionary)));
|
|
3004
|
+
}
|
|
3005
|
+
/**
|
|
3006
|
+
* Adds an object to the document and returns its view.
|
|
3007
|
+
*
|
|
3008
|
+
* @param isa The object's kind; written as the `isa` property.
|
|
3009
|
+
* @param properties The object's remaining properties. The dictionary is
|
|
3010
|
+
* stored as passed (not copied), with `isa` written first so the
|
|
3011
|
+
* serialized entry leads with it.
|
|
3012
|
+
* @param seed Seed for the deterministic id; defaults to the isa, which
|
|
3013
|
+
* is only sensible for singleton objects.
|
|
3014
|
+
* @returns The view of the created object.
|
|
3015
|
+
*/
|
|
3016
|
+
add(isa, properties, seed) {
|
|
3017
|
+
const id = this.generateId(seed ?? isa);
|
|
3018
|
+
this.objectsDictionary[id] = {
|
|
3019
|
+
isa,
|
|
3020
|
+
...properties
|
|
3021
|
+
};
|
|
3022
|
+
const view = this.createView(id, isa);
|
|
3023
|
+
this.views.set(id, view);
|
|
3024
|
+
return view;
|
|
3025
|
+
}
|
|
3026
|
+
/**
|
|
3027
|
+
* The view of the document's root `PBXProject` object.
|
|
3028
|
+
*
|
|
3029
|
+
* @throws XcodeModelError when `rootObject` is missing or dangling,
|
|
3030
|
+
* since without it no project-level operation is meaningful.
|
|
3031
|
+
*/
|
|
3032
|
+
get rootProject() {
|
|
3033
|
+
const view = this.get(asString(this.document["rootObject"]));
|
|
3034
|
+
if (!(view instanceof RootProject)) throw new XcodeModelError("The document's rootObject does not reference a PBXProject");
|
|
3035
|
+
return view;
|
|
3036
|
+
}
|
|
3037
|
+
/**
|
|
3038
|
+
* The views of the project's targets of every kind (native, aggregate,
|
|
3039
|
+
* and legacy), in project order.
|
|
3040
|
+
*/
|
|
3041
|
+
targets() {
|
|
3042
|
+
const targets = [];
|
|
3043
|
+
for (const id of this.rootProject.targetIds()) {
|
|
3044
|
+
const view = this.get(id);
|
|
3045
|
+
if (view instanceof Target) targets.push(view);
|
|
3046
|
+
}
|
|
3047
|
+
return targets;
|
|
3048
|
+
}
|
|
3049
|
+
/**
|
|
3050
|
+
* The views of the project's native targets, in project order. Targets
|
|
3051
|
+
* of other kinds (aggregate and legacy targets) are not included.
|
|
3052
|
+
*/
|
|
3053
|
+
nativeTargets() {
|
|
3054
|
+
return this.targets().filter((target) => target instanceof NativeTarget);
|
|
3055
|
+
}
|
|
3056
|
+
/**
|
|
3057
|
+
* Finds a native target by name.
|
|
3058
|
+
*/
|
|
3059
|
+
findTarget(name) {
|
|
3060
|
+
return this.nativeTargets().find((target) => target.name === name);
|
|
3061
|
+
}
|
|
3062
|
+
/**
|
|
3063
|
+
* Finds the main application target for a platform. It prefers the
|
|
3064
|
+
* application target whose own configurations carry the platform's
|
|
3065
|
+
* deployment-target key, and falls back to the first application target
|
|
3066
|
+
* in project order. Returns `undefined` when the project has no
|
|
3067
|
+
* application target.
|
|
3068
|
+
*/
|
|
3069
|
+
findMainAppTarget(platform = "ios") {
|
|
3070
|
+
const deploymentKey = DEPLOYMENT_TARGET_KEY[platform];
|
|
3071
|
+
const applications = this.nativeTargets().filter((target) => target.productType === ProductType.application);
|
|
3072
|
+
return applications.find((target) => target.buildConfigurations().some((configuration) => {
|
|
3073
|
+
const settings = asDictionary(configuration.properties["buildSettings"]);
|
|
3074
|
+
return settings != null && deploymentKey in settings;
|
|
3075
|
+
})) ?? applications[0];
|
|
3076
|
+
}
|
|
3077
|
+
/**
|
|
3078
|
+
* Creates a native target with its build configurations, product file
|
|
3079
|
+
* reference in the Products group, and the standard Sources, Frameworks,
|
|
3080
|
+
* and Resources build phases, and registers it on the project.
|
|
3081
|
+
*
|
|
3082
|
+
* @throws XcodeModelError for product types the model cannot create a
|
|
3083
|
+
* product reference for; see {@link AddNativeTargetOptions.productType}.
|
|
3084
|
+
*/
|
|
3085
|
+
addNativeTarget(options) {
|
|
3086
|
+
const productInfo = PRODUCT_FILE_INFO[options.productType];
|
|
3087
|
+
if (productInfo == null) throw new XcodeModelError(`Cannot create a product reference for product type ${options.productType}`);
|
|
3088
|
+
const configurationIds = (options.configurationNames ?? ["Debug", "Release"]).map((configurationName) => this.add(Isa.buildConfiguration, {
|
|
3089
|
+
buildSettings: { ...options.buildSettings },
|
|
3090
|
+
name: configurationName
|
|
3091
|
+
}, `${Isa.buildConfiguration} ${options.name} ${configurationName}`).id);
|
|
3092
|
+
const configurationList = this.add(Isa.configurationList, {
|
|
3093
|
+
buildConfigurations: configurationIds,
|
|
3094
|
+
defaultConfigurationIsVisible: 0,
|
|
3095
|
+
defaultConfigurationName: options.defaultConfigurationName ?? "Release"
|
|
3096
|
+
}, `${Isa.configurationList} ${options.name}`);
|
|
3097
|
+
const productReference = this.add(Isa.fileReference, {
|
|
3098
|
+
explicitFileType: productInfo.fileType,
|
|
3099
|
+
includeInIndex: 0,
|
|
3100
|
+
path: `${options.name}${productInfo.extension}`,
|
|
3101
|
+
sourceTree: "BUILT_PRODUCTS_DIR"
|
|
3102
|
+
}, `${Isa.fileReference} ${options.name}${productInfo.extension}`);
|
|
3103
|
+
this.rootProject.ensureProductsGroup().addChild(productReference);
|
|
3104
|
+
const target = this.add(Isa.nativeTarget, {
|
|
3105
|
+
buildConfigurationList: configurationList.id,
|
|
3106
|
+
buildPhases: [],
|
|
3107
|
+
buildRules: [],
|
|
3108
|
+
dependencies: [],
|
|
3109
|
+
name: options.name,
|
|
3110
|
+
productName: options.name,
|
|
3111
|
+
productReference: productReference.id,
|
|
3112
|
+
productType: options.productType
|
|
3113
|
+
}, `${Isa.nativeTarget} ${options.name}`);
|
|
3114
|
+
ensureArray(this.rootProject.properties, "targets").push(target.id);
|
|
3115
|
+
const nativeTarget = target;
|
|
3116
|
+
nativeTarget.ensureSourcesPhase();
|
|
3117
|
+
nativeTarget.ensureFrameworksPhase();
|
|
3118
|
+
nativeTarget.ensureResourcesPhase();
|
|
3119
|
+
return nativeTarget;
|
|
3120
|
+
}
|
|
3121
|
+
/**
|
|
3122
|
+
* Finds the remote Swift package reference for a repository URL.
|
|
3123
|
+
*/
|
|
3124
|
+
findSwiftPackage(repositoryUrl) {
|
|
3125
|
+
for (const id of stringItems(this.rootProject.properties["packageReferences"])) {
|
|
3126
|
+
const reference = this.get(id);
|
|
3127
|
+
if (reference?.getString("repositoryURL") === repositoryUrl) return reference;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
/**
|
|
3131
|
+
* Adds a remote Swift package reference and registers it on the project.
|
|
3132
|
+
* When the project already references the repository, the existing
|
|
3133
|
+
* reference is returned unchanged, requirement included; adjust an
|
|
3134
|
+
* existing requirement through the reference's properties. Link products
|
|
3135
|
+
* to targets with {@link NativeTarget.addSwiftPackageProduct}.
|
|
3136
|
+
*
|
|
3137
|
+
* @param options.repositoryURL The package's git URL.
|
|
3138
|
+
* @param options.requirement The version requirement dictionary as Xcode
|
|
3139
|
+
* stores it, for example
|
|
3140
|
+
* `{ kind: "upToNextMajorVersion", minimumVersion: "5.0.0" }`.
|
|
3141
|
+
* @returns The view of the package reference for the repository.
|
|
3142
|
+
*/
|
|
3143
|
+
addSwiftPackage(options) {
|
|
3144
|
+
const existing = this.findSwiftPackage(options.repositoryURL);
|
|
3145
|
+
if (existing != null) return existing;
|
|
3146
|
+
const reference = this.add(Isa.remoteSwiftPackageReference, {
|
|
3147
|
+
repositoryURL: options.repositoryURL,
|
|
3148
|
+
requirement: options.requirement
|
|
3149
|
+
}, `${Isa.remoteSwiftPackageReference} ${options.repositoryURL}`);
|
|
3150
|
+
ensureArray(this.rootProject.properties, "packageReferences").push(reference.id);
|
|
3151
|
+
return reference;
|
|
3152
|
+
}
|
|
3153
|
+
/**
|
|
3154
|
+
* Finds the local Swift package reference for a directory path.
|
|
3155
|
+
*/
|
|
3156
|
+
findLocalSwiftPackage(relativePath) {
|
|
3157
|
+
return this.rootProject.packageReferences().find((reference) => reference.isa === Isa.localSwiftPackageReference && reference.getString("relativePath") === relativePath);
|
|
3158
|
+
}
|
|
3159
|
+
/**
|
|
3160
|
+
* Adds a local (path-based) Swift package reference and registers it on
|
|
3161
|
+
* the project, returning the existing reference when the path is already
|
|
3162
|
+
* registered. Products link to targets the same way as remote packages,
|
|
3163
|
+
* through {@link NativeTarget.addSwiftPackageProduct}.
|
|
3164
|
+
*
|
|
3165
|
+
* @param relativePath The package directory, relative to the project.
|
|
3166
|
+
* @returns The view of the package reference for the path.
|
|
3167
|
+
*/
|
|
3168
|
+
addLocalSwiftPackage(relativePath) {
|
|
3169
|
+
const existing = this.findLocalSwiftPackage(relativePath);
|
|
3170
|
+
if (existing != null) return existing;
|
|
3171
|
+
const reference = this.add(Isa.localSwiftPackageReference, { relativePath }, `${Isa.localSwiftPackageReference} ${relativePath}`);
|
|
3172
|
+
ensureArray(this.rootProject.properties, "packageReferences").push(reference.id);
|
|
3173
|
+
return reference;
|
|
3174
|
+
}
|
|
3175
|
+
/**
|
|
3176
|
+
* The views of every `PBXBuildFile` that points at the referenced object
|
|
3177
|
+
* through `fileRef` or `productRef`. Useful for relocating a product
|
|
3178
|
+
* between copy phases.
|
|
3179
|
+
*/
|
|
3180
|
+
buildFilesReferencing(reference) {
|
|
3181
|
+
const buildFiles = [];
|
|
3182
|
+
for (const [, view] of this.objects()) if (view.isa === Isa.buildFile && (view.getString("fileRef") === reference.id || view.getString("productRef") === reference.id)) buildFiles.push(view);
|
|
3183
|
+
return buildFiles;
|
|
3184
|
+
}
|
|
3185
|
+
/**
|
|
3186
|
+
* Validates the document's object graph and returns every problem
|
|
3187
|
+
* found. An empty array means the graph is structurally sound. The
|
|
3188
|
+
* checks cover the root object, object kinds, known references, and
|
|
3189
|
+
* reachability.
|
|
3190
|
+
*/
|
|
3191
|
+
validate() {
|
|
3192
|
+
return validateProject(this);
|
|
3193
|
+
}
|
|
3194
|
+
/**
|
|
3195
|
+
* Removes every object unreachable from the root object and returns the
|
|
3196
|
+
* removed ids. Reachability is conservative. Any real reference keeps
|
|
3197
|
+
* an object alive, even through properties outside the known schema.
|
|
3198
|
+
*/
|
|
3199
|
+
pruneOrphans() {
|
|
3200
|
+
return pruneOrphanObjects(this);
|
|
3201
|
+
}
|
|
3202
|
+
/**
|
|
3203
|
+
* The views of every object that references the id anywhere in its
|
|
3204
|
+
* properties. A reference is a string property naming the id, an id
|
|
3205
|
+
* list containing it, or a nested dictionary carrying it as a key or
|
|
3206
|
+
* string value.
|
|
3207
|
+
*
|
|
3208
|
+
* The scan is linear over the document; removal flows call it once per
|
|
3209
|
+
* removed object, which keeps teardown proportional to what is actually
|
|
3210
|
+
* removed.
|
|
3211
|
+
*/
|
|
3212
|
+
referrersOf(id) {
|
|
3213
|
+
const referrers = [];
|
|
3214
|
+
for (const [referrerId, view] of this.objects()) if (referrerId !== id && objectReferences(view.properties, id)) referrers.push(view);
|
|
3215
|
+
return referrers;
|
|
3216
|
+
}
|
|
3217
|
+
/**
|
|
3218
|
+
* Removes an object from the document and strips every reference to it
|
|
3219
|
+
* from the remaining objects. String properties naming the id are
|
|
3220
|
+
* deleted, id lists drop it, and nested dictionaries keyed by object id
|
|
3221
|
+
* (such as `TargetAttributes`) drop its entry.
|
|
3222
|
+
*
|
|
3223
|
+
* Removing an id the document does not contain is a no-op. This is the
|
|
3224
|
+
* low-level removal; {@link removeTarget} composes it into a full
|
|
3225
|
+
* teardown.
|
|
3226
|
+
*/
|
|
3227
|
+
removeObject(id) {
|
|
3228
|
+
if (!(id in this.objectsDictionary)) return;
|
|
3229
|
+
delete this.objectsDictionary[id];
|
|
3230
|
+
this.views.delete(id);
|
|
3231
|
+
for (const [, view] of this.objects()) stripReferences(view.properties, id);
|
|
3232
|
+
}
|
|
3233
|
+
/**
|
|
3234
|
+
* Removes a target and everything that exists only for its sake. That
|
|
3235
|
+
* covers its build phases and their build files, its configuration list
|
|
3236
|
+
* and configurations, its product reference and the build files
|
|
3237
|
+
* embedding it, dependency objects and container proxies in both
|
|
3238
|
+
* directions (other targets' dependencies on it, and its own
|
|
3239
|
+
* dependencies on others), its membership exception sets, and
|
|
3240
|
+
* synchronized folders no remaining target links.
|
|
3241
|
+
*
|
|
3242
|
+
* On-disk sources are untouched; the removal is document-only, like
|
|
3243
|
+
* deleting a target in Xcode and keeping its folder.
|
|
3244
|
+
*
|
|
3245
|
+
* @throws XcodeModelError when the target belongs to another project,
|
|
3246
|
+
* since removing by another document's ids would tear down unrelated
|
|
3247
|
+
* objects that happen to share them.
|
|
3248
|
+
*/
|
|
3249
|
+
removeTarget(target) {
|
|
3250
|
+
if (target.project !== this) throw new XcodeModelError("Cannot remove a target that belongs to another project");
|
|
3251
|
+
const ownedIds = /* @__PURE__ */ new Set();
|
|
3252
|
+
for (const phase of target.buildPhases()) {
|
|
3253
|
+
for (const buildFileId of phase.buildFileIds) ownedIds.add(buildFileId);
|
|
3254
|
+
ownedIds.add(phase.id);
|
|
3255
|
+
}
|
|
3256
|
+
const configurationListId = target.getString("buildConfigurationList");
|
|
3257
|
+
if (configurationListId != null) {
|
|
3258
|
+
for (const configuration of target.buildConfigurations()) ownedIds.add(configuration.id);
|
|
3259
|
+
ownedIds.add(configurationListId);
|
|
3260
|
+
}
|
|
3261
|
+
const product = this.get(target.getString("productReference"));
|
|
3262
|
+
if (product != null) {
|
|
3263
|
+
for (const buildFile of this.buildFilesReferencing(product)) ownedIds.add(buildFile.id);
|
|
3264
|
+
ownedIds.add(product.id);
|
|
3265
|
+
}
|
|
3266
|
+
for (const dependency of target.dependencies()) {
|
|
3267
|
+
const proxyId = dependency.getString("targetProxy");
|
|
3268
|
+
if (proxyId != null) ownedIds.add(proxyId);
|
|
3269
|
+
ownedIds.add(dependency.id);
|
|
3270
|
+
}
|
|
3271
|
+
for (const [, view] of this.objects()) {
|
|
3272
|
+
if (view.isa === Isa.targetDependency && view.getString("target") === target.id) {
|
|
3273
|
+
const proxyId = view.getString("targetProxy");
|
|
3274
|
+
if (proxyId != null) ownedIds.add(proxyId);
|
|
3275
|
+
ownedIds.add(view.id);
|
|
3276
|
+
}
|
|
3277
|
+
if (view.isa === Isa.fileSystemSynchronizedBuildFileExceptionSet && view.getString("target") === target.id) ownedIds.add(view.id);
|
|
3278
|
+
}
|
|
3279
|
+
const syncGroupIds = stringItems(target.properties["fileSystemSynchronizedGroups"]);
|
|
3280
|
+
this.removeObject(target.id);
|
|
3281
|
+
for (const id of ownedIds) this.removeObject(id);
|
|
3282
|
+
for (const groupId of syncGroupIds) if (!this.nativeTargets().some((remaining) => stringItems(remaining.properties["fileSystemSynchronizedGroups"]).includes(groupId))) this.removeObject(groupId);
|
|
3283
|
+
}
|
|
3284
|
+
/**
|
|
3285
|
+
* Finds a file reference by its project-relative path, resolving each
|
|
3286
|
+
* reference's location through the group tree from the main group
|
|
3287
|
+
* (nested group `path` components join with `/`).
|
|
3288
|
+
*
|
|
3289
|
+
* Members of file-system-synchronized folders are not listed in the
|
|
3290
|
+
* document and therefore cannot be found; check
|
|
3291
|
+
* {@link NativeTarget.syncGroupPaths} for folder-level containment
|
|
3292
|
+
* instead.
|
|
3293
|
+
*/
|
|
3294
|
+
findFileReference(projectRelativePath) {
|
|
3295
|
+
const mainGroup = this.rootProject.mainGroup();
|
|
3296
|
+
if (mainGroup == null) return;
|
|
3297
|
+
const visited = /* @__PURE__ */ new Set();
|
|
3298
|
+
const search = (group, prefix) => {
|
|
3299
|
+
if (visited.has(group.id)) return;
|
|
3300
|
+
visited.add(group.id);
|
|
3301
|
+
for (const childId of group.childIds) {
|
|
3302
|
+
const child = this.get(childId);
|
|
3303
|
+
if (child == null) continue;
|
|
3304
|
+
const childPath = child.getString("path");
|
|
3305
|
+
if (child instanceof Group) {
|
|
3306
|
+
const found = search(child, childPath == null ? prefix : joinPath(prefix, childPath));
|
|
3307
|
+
if (found != null) return found;
|
|
3308
|
+
} else if (child.isa === Isa.fileReference && childPath != null && joinPath(prefix, childPath) === projectRelativePath) return child;
|
|
3309
|
+
}
|
|
3310
|
+
};
|
|
3311
|
+
return search(mainGroup, "");
|
|
3312
|
+
}
|
|
3313
|
+
/**
|
|
3314
|
+
* Creates the typed view for an object, dispatching on its isa. Objects
|
|
3315
|
+
* outside the typed vocabulary get the generic base view.
|
|
3316
|
+
*/
|
|
3317
|
+
createView(id, isa) {
|
|
3318
|
+
switch (isa) {
|
|
3319
|
+
case Isa.nativeTarget: return new NativeTarget(this, id);
|
|
3320
|
+
case Isa.aggregateTarget: return new AggregateTarget(this, id);
|
|
3321
|
+
case Isa.legacyTarget: return new LegacyTarget(this, id);
|
|
3322
|
+
case Isa.project: return new RootProject(this, id);
|
|
3323
|
+
case Isa.group:
|
|
3324
|
+
case Isa.variantGroup: return new Group(this, id);
|
|
3325
|
+
case Isa.versionGroup: return new VersionGroup(this, id);
|
|
3326
|
+
case Isa.fileSystemSynchronizedRootGroup: return new SyncRootGroup(this, id);
|
|
3327
|
+
case Isa.buildRule: return new BuildRule(this, id);
|
|
3328
|
+
case Isa.referenceProxy: return new ReferenceProxy(this, id);
|
|
3329
|
+
default: return isa.endsWith("BuildPhase") ? new BuildPhase(this, id) : new XcodeObject(this, id);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
};
|
|
3333
|
+
/**
|
|
3334
|
+
* Joins two path segments with a `/`, treating an empty prefix as the
|
|
3335
|
+
* project root.
|
|
3336
|
+
*/
|
|
3337
|
+
function joinPath(prefix, segment) {
|
|
3338
|
+
return prefix === "" ? segment : `${prefix}/${segment}`;
|
|
3339
|
+
}
|
|
3340
|
+
/**
|
|
3341
|
+
* Whether a value references the id anywhere in its structure. A
|
|
3342
|
+
* reference is a string equal to it, an array containing it at any depth,
|
|
3343
|
+
* or a dictionary carrying it as a key or somewhere in its values.
|
|
3344
|
+
*/
|
|
3345
|
+
function valueReferences(value, id) {
|
|
3346
|
+
if (typeof value === "string") return value === id;
|
|
3347
|
+
if (Array.isArray(value)) return value.some((item) => valueReferences(item, id));
|
|
3348
|
+
const nested = asDictionary(value);
|
|
3349
|
+
if (nested != null) return id in nested || objectReferences(nested, id);
|
|
3350
|
+
return false;
|
|
3351
|
+
}
|
|
3352
|
+
/**
|
|
3353
|
+
* Whether an object's properties reference the id anywhere. See
|
|
3354
|
+
* {@link valueReferences} for the shapes considered.
|
|
3355
|
+
*/
|
|
3356
|
+
function objectReferences(properties, id) {
|
|
3357
|
+
for (const key of Object.keys(properties)) if (valueReferences(properties[key], id)) return true;
|
|
3358
|
+
return false;
|
|
3359
|
+
}
|
|
3360
|
+
/**
|
|
3361
|
+
* Strips every reference to the id inside a value and returns the value
|
|
3362
|
+
* to keep. Strings equal to the id become `undefined`, arrays drop
|
|
3363
|
+
* matching items at any depth, and dictionaries drop entries keyed by it
|
|
3364
|
+
* and recurse into their values.
|
|
3365
|
+
*/
|
|
3366
|
+
function stripValue(value, id) {
|
|
3367
|
+
if (typeof value === "string") return value === id ? void 0 : value;
|
|
3368
|
+
if (Array.isArray(value)) {
|
|
3369
|
+
let changed = false;
|
|
3370
|
+
const kept = [];
|
|
3371
|
+
for (const item of value) {
|
|
3372
|
+
const stripped = stripValue(item, id);
|
|
3373
|
+
if (stripped == null || stripped !== item) changed = true;
|
|
3374
|
+
if (stripped != null) kept.push(stripped);
|
|
3375
|
+
}
|
|
3376
|
+
return changed ? kept : value;
|
|
3377
|
+
}
|
|
3378
|
+
const nested = asDictionary(value);
|
|
3379
|
+
if (nested != null) {
|
|
3380
|
+
if (id in nested) delete nested[id];
|
|
3381
|
+
stripReferences(nested, id);
|
|
3382
|
+
}
|
|
3383
|
+
return value;
|
|
3384
|
+
}
|
|
3385
|
+
/**
|
|
3386
|
+
* Strips every reference to the id from an object's properties; see
|
|
3387
|
+
* {@link stripValue} for the shapes handled. String properties naming the
|
|
3388
|
+
* id are deleted rather than left empty.
|
|
3389
|
+
*/
|
|
3390
|
+
function stripReferences(properties, id) {
|
|
3391
|
+
for (const key of Object.keys(properties)) {
|
|
3392
|
+
const value = properties[key];
|
|
3393
|
+
if (value == null) continue;
|
|
3394
|
+
const stripped = stripValue(value, id);
|
|
3395
|
+
if (stripped == null) delete properties[key];
|
|
3396
|
+
else if (stripped !== value) properties[key] = stripped;
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3399
|
+
//#endregion
|
|
3400
|
+
export { AggregateTarget, BuildPhase, BuildRule, CopyFilesDestination, Group, Isa, LegacyTarget, NativeTarget, PbxprojBuildError, PbxprojParseError, ProductType, ReferenceProxy, RootProject, SyncRootGroup, Target, VersionGroup, XcodeModelError, XcodeObject, XcodeProject, buildPbxproj, generateObjectId, parsePbxproj };
|