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/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
- export { PbxprojBuildError, PbxprojParseError, buildPbxproj, parsePbxproj };
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 };