Skip to content

Commit a2ca454

Browse files
EvanBaconclaude
andauthored
Fix pbxproj output differences (keys not sorted, extra properties) (#46)
* Avoid default file props; stable JSON key ordering Stop auto-populating PBXFileReference defaults (fileEncoding, includeInIndex) and avoid initializing empty input/output path arrays in PBX build phases so the library doesn't add properties Xcode omits when round-tripping projects. Add deterministic JSON writer ordering (case-sensitive ASCII with "isa" first) for stable output. Update tests to reflect the removed defaults and to skip fixtures that rely on original non-sorted key order. * Update fixtures with sorted keys instead of skipping tests Re-process project.pbxproj and project-with-entitlements.pbxproj through the parser/writer to normalize key ordering, then restore them in inOutFixtures array. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d9ea6c3 commit a2ca454

File tree

6 files changed

+33
-59
lines changed

6 files changed

+33
-59
lines changed

src/api/PBXFileReference.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -103,12 +103,9 @@ export class PBXFileReference extends AbstractObject<PBXFileReferenceModel> {
103103
}
104104

105105
protected setupDefaults() {
106-
if (this.props.fileEncoding == null) {
107-
this.props.fileEncoding = 4;
108-
}
109-
// if (this.sourceTree == null) {
110-
// this.sourceTree = "SOURCE_ROOT";
111-
// }
106+
// Note: fileEncoding and includeInIndex are intentionally NOT set as defaults.
107+
// Xcode only includes these properties when explicitly set. Setting them
108+
// automatically would cause unnecessary changes when round-tripping projects.
112109

113110
if (
114111
!this.props.lastKnownFileType &&
@@ -118,10 +115,6 @@ export class PBXFileReference extends AbstractObject<PBXFileReferenceModel> {
118115
this.setLastKnownFileType();
119116
}
120117

121-
if (this.props.includeInIndex == null) {
122-
this.props.includeInIndex = 0;
123-
}
124-
125118
if (this.props.name == null && this.props.path) {
126119
const name = path.basename(this.props.path);
127120
// If the values are the same then skip setting name.
@@ -132,11 +125,6 @@ export class PBXFileReference extends AbstractObject<PBXFileReferenceModel> {
132125
if (!this.props.sourceTree) {
133126
this.props.sourceTree = getPossibleDefaultSourceTree(this.props);
134127
}
135-
136-
// Clear the includeInIndex flag for framework files
137-
if (this.props.path && path.extname(this.props.path) === ".framework") {
138-
this.props.includeInIndex = undefined;
139-
}
140128
}
141129
getParent() {
142130
return getParent(this);

src/api/PBXSourcesBuildPhase.ts

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -246,18 +246,9 @@ export class PBXShellScriptBuildPhase extends AbstractBuildPhase<
246246
this.props.shellScript =
247247
"# Type a script or drag a script file from your workspace to insert its path.\n";
248248
}
249-
if (!this.props.outputFileListPaths) {
250-
this.props.outputFileListPaths = [];
251-
}
252-
if (!this.props.outputPaths) {
253-
this.props.outputPaths = [];
254-
}
255-
if (!this.props.inputFileListPaths) {
256-
this.props.inputFileListPaths = [];
257-
}
258-
if (!this.props.inputPaths) {
259-
this.props.inputPaths = [];
260-
}
249+
// Note: inputPaths, outputPaths, inputFileListPaths, outputFileListPaths
250+
// are intentionally NOT initialized to empty arrays. Xcode omits these
251+
// properties when they are empty.
261252
super.setupDefaults();
262253
}
263254
}

src/api/__tests__/PBXFileReference.test.ts

Lines changed: 7 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,6 @@ describe("PBXFileReference", () => {
7575
expect(ref.uuid).toBe("XX4DFF38D47332D6BF0183XX");
7676

7777
expect(ref.props).toEqual({
78-
fileEncoding: 4,
79-
includeInIndex: undefined,
8078
isa: "PBXFileReference",
8179
name: "SwiftUI.framework",
8280
path: "System/Library/Frameworks/SwiftUI.framework",
@@ -85,28 +83,23 @@ describe("PBXFileReference", () => {
8583
});
8684
});
8785

88-
it("should set default file encoding", () => {
89-
const xcproj = XcodeProject.open(WORKING_FIXTURE);
90-
const ref = PBXFileReference.create(xcproj, {
91-
path: "test.swift",
92-
});
86+
// Note: fileEncoding and includeInIndex are no longer set by default
87+
// to avoid adding properties when round-tripping projects.
88+
// Users can set these explicitly if needed.
9389

94-
expect(ref.props.fileEncoding).toBe(4);
95-
});
96-
97-
it("should set includeInIndex for regular files", () => {
90+
it("should not set fileEncoding by default", () => {
9891
const xcproj = XcodeProject.open(WORKING_FIXTURE);
9992
const ref = PBXFileReference.create(xcproj, {
10093
path: "test.swift",
10194
});
10295

103-
expect(ref.props.includeInIndex).toBe(0);
96+
expect(ref.props.fileEncoding).toBeUndefined();
10497
});
10598

106-
it("should clear includeInIndex for framework files", () => {
99+
it("should not set includeInIndex by default", () => {
107100
const xcproj = XcodeProject.open(WORKING_FIXTURE);
108101
const ref = PBXFileReference.create(xcproj, {
109-
path: "TestFramework.framework",
102+
path: "test.swift",
110103
});
111104

112105
expect(ref.props.includeInIndex).toBeUndefined();
@@ -141,8 +134,6 @@ describe("PBXFileReference", () => {
141134
});
142135

143136
expect(ref.props).toEqual({
144-
fileEncoding: 4,
145-
includeInIndex: 0,
146137
isa: "PBXFileReference",
147138
lastKnownFileType: "sourcecode.swift",
148139
name: "funky.swift",
@@ -157,8 +148,6 @@ describe("PBXFileReference", () => {
157148
});
158149

159150
expect(ref.props).toEqual({
160-
fileEncoding: 4,
161-
includeInIndex: 0,
162151
isa: "PBXFileReference",
163152
lastKnownFileType: "text.css",
164153
name: "funky.css",
@@ -173,8 +162,6 @@ describe("PBXFileReference", () => {
173162
});
174163

175164
expect(ref.props).toEqual({
176-
fileEncoding: 4,
177-
includeInIndex: 0,
178165
isa: "PBXFileReference",
179166
lastKnownFileType: "text.html",
180167
name: "funky.html",
@@ -189,8 +176,6 @@ describe("PBXFileReference", () => {
189176
});
190177

191178
expect(ref.props).toEqual({
192-
fileEncoding: 4,
193-
includeInIndex: 0,
194179
isa: "PBXFileReference",
195180
lastKnownFileType: "text.json",
196181
name: "funky.json",
@@ -205,8 +190,6 @@ describe("PBXFileReference", () => {
205190
});
206191

207192
expect(ref.props).toEqual({
208-
fileEncoding: 4,
209-
includeInIndex: 0,
210193
isa: "PBXFileReference",
211194
lastKnownFileType: "sourcecode.javascript",
212195
name: "funky.js",
@@ -221,8 +204,6 @@ describe("PBXFileReference", () => {
221204
});
222205

223206
expect(ref.props).toEqual({
224-
fileEncoding: 4,
225-
includeInIndex: 0,
226207
isa: "PBXFileReference",
227208
name: "funky",
228209
path: "fun/funky",

src/json/__tests__/fixtures/project-with-entitlements.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@
276276
buildSettings = {
277277
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
278278
CLANG_ENABLE_MODULES = YES;
279+
CODE_SIGN_ENTITLEMENTS = testapp/example.entitlements;
279280
CURRENT_PROJECT_VERSION = 1;
280281
ENABLE_BITCODE = NO;
281282
GCC_PREPROCESSOR_DEFINITIONS = (
@@ -290,7 +291,6 @@
290291
"-ObjC",
291292
"-lc++",
292293
);
293-
CODE_SIGN_ENTITLEMENTS = testapp/example.entitlements;
294294
PRODUCT_BUNDLE_IDENTIFIER = org.name.testproject;
295295
PRODUCT_NAME = testproject;
296296
SWIFT_OPTIMIZATION_LEVEL = "-Onone";
@@ -305,6 +305,7 @@
305305
buildSettings = {
306306
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
307307
CLANG_ENABLE_MODULES = YES;
308+
CODE_SIGN_ENTITLEMENTS = testapp/example.entitlements;
308309
CURRENT_PROJECT_VERSION = 1;
309310
INFOPLIST_FILE = testproject/Info.plist;
310311
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
@@ -314,7 +315,6 @@
314315
"-ObjC",
315316
"-lc++",
316317
);
317-
CODE_SIGN_ENTITLEMENTS = testapp/example.entitlements;
318318
PRODUCT_BUNDLE_IDENTIFIER = org.name.testproject;
319319
PRODUCT_NAME = testproject;
320320
SWIFT_VERSION = 5.0;

src/json/__tests__/fixtures/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -352,7 +352,6 @@
352352
COPY_PHASE_STRIP = NO;
353353
ENABLE_STRICT_OBJC_MSGSEND = YES;
354354
ENABLE_TESTABILITY = YES;
355-
TARGETED_DEVICE_FAMILY = "1,2";
356355
GCC_C_LANGUAGE_STANDARD = gnu99;
357356
GCC_DYNAMIC_NO_PIC = NO;
358357
GCC_NO_COMMON_BLOCKS = YES;
@@ -378,6 +377,7 @@
378377
MTL_ENABLE_DEBUG_INFO = YES;
379378
ONLY_ACTIVE_ARCH = YES;
380379
SDKROOT = iphoneos;
380+
TARGETED_DEVICE_FAMILY = "1,2";
381381
};
382382
name = Debug;
383383
};
@@ -421,7 +421,6 @@
421421
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
422422
GCC_WARN_UNUSED_FUNCTION = YES;
423423
GCC_WARN_UNUSED_VARIABLE = YES;
424-
TARGETED_DEVICE_FAMILY = 1;
425424
IPHONEOS_DEPLOYMENT_TARGET = 10.0;
426425
LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)";
427426
LIBRARY_SEARCH_PATHS = (
@@ -431,6 +430,7 @@
431430
);
432431
MTL_ENABLE_DEBUG_INFO = NO;
433432
SDKROOT = iphoneos;
433+
TARGETED_DEVICE_FAMILY = 1;
434434
VALIDATE_PRODUCT = YES;
435435
};
436436
name = Release;

src/json/writer.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,14 @@ export class Writer {
176176
}
177177

178178
private writeObject(object: JSONObject, isBase?: boolean) {
179-
Object.entries(object).forEach(([key, value]) => {
179+
Object.entries(object)
180+
.sort(([a], [b]) => {
181+
// isa always comes first, then case-sensitive ASCII alphabetical
182+
if (a === "isa") return -1;
183+
if (b === "isa") return 1;
184+
return a < b ? -1 : a > b ? 1 : 0;
185+
})
186+
.forEach(([key, value]) => {
180187
if (this.options.skipNullishValues && value == null) {
181188
return;
182189
} else if (value instanceof Buffer) {
@@ -281,7 +288,14 @@ export class Writer {
281288
) => {
282289
line.push(this.formatId(key) + " = {");
283290

284-
Object.entries(value).forEach(([key, obj]) => {
291+
Object.entries(value)
292+
.sort(([a], [b]) => {
293+
// isa always comes first, then case-sensitive ASCII alphabetical
294+
if (a === "isa") return -1;
295+
if (b === "isa") return 1;
296+
return a < b ? -1 : a > b ? 1 : 0;
297+
})
298+
.forEach(([key, obj]) => {
285299
if (this.options.skipNullishValues && obj == null) {
286300
return;
287301
} else if (obj instanceof Buffer) {

0 commit comments

Comments
 (0)