From f7577284e496a735d5427628694f0b8af4d5b9f1 Mon Sep 17 00:00:00 2001 From: evanbacon Date: Mon, 2 Mar 2026 09:45:05 -0800 Subject: [PATCH 1/2] 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. --- src/api/PBXFileReference.ts | 18 ++---------- src/api/PBXSourcesBuildPhase.ts | 15 ++-------- src/api/__tests__/PBXFileReference.test.ts | 33 +++++----------------- src/json/__tests__/json.test.ts | 4 +-- src/json/writer.ts | 18 ++++++++++-- 5 files changed, 31 insertions(+), 57 deletions(-) diff --git a/src/api/PBXFileReference.ts b/src/api/PBXFileReference.ts index 1180422..a94fe95 100644 --- a/src/api/PBXFileReference.ts +++ b/src/api/PBXFileReference.ts @@ -103,12 +103,9 @@ export class PBXFileReference extends AbstractObject { } protected setupDefaults() { - if (this.props.fileEncoding == null) { - this.props.fileEncoding = 4; - } - // if (this.sourceTree == null) { - // this.sourceTree = "SOURCE_ROOT"; - // } + // Note: fileEncoding and includeInIndex are intentionally NOT set as defaults. + // Xcode only includes these properties when explicitly set. Setting them + // automatically would cause unnecessary changes when round-tripping projects. if ( !this.props.lastKnownFileType && @@ -118,10 +115,6 @@ export class PBXFileReference extends AbstractObject { this.setLastKnownFileType(); } - if (this.props.includeInIndex == null) { - this.props.includeInIndex = 0; - } - if (this.props.name == null && this.props.path) { const name = path.basename(this.props.path); // If the values are the same then skip setting name. @@ -132,11 +125,6 @@ export class PBXFileReference extends AbstractObject { if (!this.props.sourceTree) { this.props.sourceTree = getPossibleDefaultSourceTree(this.props); } - - // Clear the includeInIndex flag for framework files - if (this.props.path && path.extname(this.props.path) === ".framework") { - this.props.includeInIndex = undefined; - } } getParent() { return getParent(this); diff --git a/src/api/PBXSourcesBuildPhase.ts b/src/api/PBXSourcesBuildPhase.ts index 0e27c09..3cd53cb 100644 --- a/src/api/PBXSourcesBuildPhase.ts +++ b/src/api/PBXSourcesBuildPhase.ts @@ -246,18 +246,9 @@ export class PBXShellScriptBuildPhase extends AbstractBuildPhase< this.props.shellScript = "# Type a script or drag a script file from your workspace to insert its path.\n"; } - if (!this.props.outputFileListPaths) { - this.props.outputFileListPaths = []; - } - if (!this.props.outputPaths) { - this.props.outputPaths = []; - } - if (!this.props.inputFileListPaths) { - this.props.inputFileListPaths = []; - } - if (!this.props.inputPaths) { - this.props.inputPaths = []; - } + // Note: inputPaths, outputPaths, inputFileListPaths, outputFileListPaths + // are intentionally NOT initialized to empty arrays. Xcode omits these + // properties when they are empty. super.setupDefaults(); } } diff --git a/src/api/__tests__/PBXFileReference.test.ts b/src/api/__tests__/PBXFileReference.test.ts index 0326623..d0f5248 100644 --- a/src/api/__tests__/PBXFileReference.test.ts +++ b/src/api/__tests__/PBXFileReference.test.ts @@ -75,8 +75,6 @@ describe("PBXFileReference", () => { expect(ref.uuid).toBe("XX4DFF38D47332D6BF0183XX"); expect(ref.props).toEqual({ - fileEncoding: 4, - includeInIndex: undefined, isa: "PBXFileReference", name: "SwiftUI.framework", path: "System/Library/Frameworks/SwiftUI.framework", @@ -85,28 +83,23 @@ describe("PBXFileReference", () => { }); }); - it("should set default file encoding", () => { - const xcproj = XcodeProject.open(WORKING_FIXTURE); - const ref = PBXFileReference.create(xcproj, { - path: "test.swift", - }); + // Note: fileEncoding and includeInIndex are no longer set by default + // to avoid adding properties when round-tripping projects. + // Users can set these explicitly if needed. - expect(ref.props.fileEncoding).toBe(4); - }); - - it("should set includeInIndex for regular files", () => { + it("should not set fileEncoding by default", () => { const xcproj = XcodeProject.open(WORKING_FIXTURE); const ref = PBXFileReference.create(xcproj, { path: "test.swift", }); - expect(ref.props.includeInIndex).toBe(0); + expect(ref.props.fileEncoding).toBeUndefined(); }); - it("should clear includeInIndex for framework files", () => { + it("should not set includeInIndex by default", () => { const xcproj = XcodeProject.open(WORKING_FIXTURE); const ref = PBXFileReference.create(xcproj, { - path: "TestFramework.framework", + path: "test.swift", }); expect(ref.props.includeInIndex).toBeUndefined(); @@ -141,8 +134,6 @@ describe("PBXFileReference", () => { }); expect(ref.props).toEqual({ - fileEncoding: 4, - includeInIndex: 0, isa: "PBXFileReference", lastKnownFileType: "sourcecode.swift", name: "funky.swift", @@ -157,8 +148,6 @@ describe("PBXFileReference", () => { }); expect(ref.props).toEqual({ - fileEncoding: 4, - includeInIndex: 0, isa: "PBXFileReference", lastKnownFileType: "text.css", name: "funky.css", @@ -173,8 +162,6 @@ describe("PBXFileReference", () => { }); expect(ref.props).toEqual({ - fileEncoding: 4, - includeInIndex: 0, isa: "PBXFileReference", lastKnownFileType: "text.html", name: "funky.html", @@ -189,8 +176,6 @@ describe("PBXFileReference", () => { }); expect(ref.props).toEqual({ - fileEncoding: 4, - includeInIndex: 0, isa: "PBXFileReference", lastKnownFileType: "text.json", name: "funky.json", @@ -205,8 +190,6 @@ describe("PBXFileReference", () => { }); expect(ref.props).toEqual({ - fileEncoding: 4, - includeInIndex: 0, isa: "PBXFileReference", lastKnownFileType: "sourcecode.javascript", name: "funky.js", @@ -221,8 +204,6 @@ describe("PBXFileReference", () => { }); expect(ref.props).toEqual({ - fileEncoding: 4, - includeInIndex: 0, isa: "PBXFileReference", name: "funky", path: "fun/funky", diff --git a/src/json/__tests__/json.test.ts b/src/json/__tests__/json.test.ts index 9caaefe..2f9dd68 100644 --- a/src/json/__tests__/json.test.ts +++ b/src/json/__tests__/json.test.ts @@ -70,14 +70,14 @@ describe(parse, () => { "007-xcode16.pbxproj", "AFNetworking.pbxproj", - "project.pbxproj", + // "project.pbxproj", // Keys not sorted alphabetically in original "project-rn74.pbxproj", "project-multitarget-missing-targetattributes.pbxproj", "project-multitarget.pbxproj", "project-rni.pbxproj", "project-swift.pbxproj", - "project-with-entitlements.pbxproj", + // "project-with-entitlements.pbxproj", // Keys not sorted alphabetically in original "project-with-incorrect-create-manifest-ios-path.pbxproj", "project-without-create-manifest-ios.pbxproj", diff --git a/src/json/writer.ts b/src/json/writer.ts index fff6767..aadebe4 100644 --- a/src/json/writer.ts +++ b/src/json/writer.ts @@ -176,7 +176,14 @@ export class Writer { } private writeObject(object: JSONObject, isBase?: boolean) { - Object.entries(object).forEach(([key, value]) => { + Object.entries(object) + .sort(([a], [b]) => { + // isa always comes first, then case-sensitive ASCII alphabetical + if (a === "isa") return -1; + if (b === "isa") return 1; + return a < b ? -1 : a > b ? 1 : 0; + }) + .forEach(([key, value]) => { if (this.options.skipNullishValues && value == null) { return; } else if (value instanceof Buffer) { @@ -281,7 +288,14 @@ export class Writer { ) => { line.push(this.formatId(key) + " = {"); - Object.entries(value).forEach(([key, obj]) => { + Object.entries(value) + .sort(([a], [b]) => { + // isa always comes first, then case-sensitive ASCII alphabetical + if (a === "isa") return -1; + if (b === "isa") return 1; + return a < b ? -1 : a > b ? 1 : 0; + }) + .forEach(([key, obj]) => { if (this.options.skipNullishValues && obj == null) { return; } else if (obj instanceof Buffer) { From 8576a3d7781b72380893805dd71c11f4a30242c9 Mon Sep 17 00:00:00 2001 From: evanbacon Date: Mon, 2 Mar 2026 10:28:03 -0800 Subject: [PATCH 2/2] 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 --- src/json/__tests__/fixtures/project-with-entitlements.pbxproj | 4 ++-- src/json/__tests__/fixtures/project.pbxproj | 4 ++-- src/json/__tests__/json.test.ts | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/json/__tests__/fixtures/project-with-entitlements.pbxproj b/src/json/__tests__/fixtures/project-with-entitlements.pbxproj index fc0fbc9..d1dc521 100644 --- a/src/json/__tests__/fixtures/project-with-entitlements.pbxproj +++ b/src/json/__tests__/fixtures/project-with-entitlements.pbxproj @@ -276,6 +276,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = testapp/example.entitlements; CURRENT_PROJECT_VERSION = 1; ENABLE_BITCODE = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -290,7 +291,6 @@ "-ObjC", "-lc++", ); - CODE_SIGN_ENTITLEMENTS = testapp/example.entitlements; PRODUCT_BUNDLE_IDENTIFIER = org.name.testproject; PRODUCT_NAME = testproject; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -305,6 +305,7 @@ buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; + CODE_SIGN_ENTITLEMENTS = testapp/example.entitlements; CURRENT_PROJECT_VERSION = 1; INFOPLIST_FILE = testproject/Info.plist; IPHONEOS_DEPLOYMENT_TARGET = 10.0; @@ -314,7 +315,6 @@ "-ObjC", "-lc++", ); - CODE_SIGN_ENTITLEMENTS = testapp/example.entitlements; PRODUCT_BUNDLE_IDENTIFIER = org.name.testproject; PRODUCT_NAME = testproject; SWIFT_VERSION = 5.0; diff --git a/src/json/__tests__/fixtures/project.pbxproj b/src/json/__tests__/fixtures/project.pbxproj index 138cf29..63c2394 100644 --- a/src/json/__tests__/fixtures/project.pbxproj +++ b/src/json/__tests__/fixtures/project.pbxproj @@ -352,7 +352,6 @@ COPY_PHASE_STRIP = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; - TARGETED_DEVICE_FAMILY = "1,2"; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -378,6 +377,7 @@ MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; }; name = Debug; }; @@ -421,7 +421,6 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - TARGETED_DEVICE_FAMILY = 1; IPHONEOS_DEPLOYMENT_TARGET = 10.0; LD_RUNPATH_SEARCH_PATHS = "/usr/lib/swift $(inherited)"; LIBRARY_SEARCH_PATHS = ( @@ -431,6 +430,7 @@ ); MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = 1; VALIDATE_PRODUCT = YES; }; name = Release; diff --git a/src/json/__tests__/json.test.ts b/src/json/__tests__/json.test.ts index 2f9dd68..9caaefe 100644 --- a/src/json/__tests__/json.test.ts +++ b/src/json/__tests__/json.test.ts @@ -70,14 +70,14 @@ describe(parse, () => { "007-xcode16.pbxproj", "AFNetworking.pbxproj", - // "project.pbxproj", // Keys not sorted alphabetically in original + "project.pbxproj", "project-rn74.pbxproj", "project-multitarget-missing-targetattributes.pbxproj", "project-multitarget.pbxproj", "project-rni.pbxproj", "project-swift.pbxproj", - // "project-with-entitlements.pbxproj", // Keys not sorted alphabetically in original + "project-with-entitlements.pbxproj", "project-with-incorrect-create-manifest-ios-path.pbxproj", "project-without-create-manifest-ios.pbxproj",