
Research
/Security News
Mini Shai-Hulud Campaign Hits Red Hat Cloud Services npm Packages
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.
@bacons/xcode
Advanced tools
@bacons/xcodeThe fastest and most accurate parser for Xcode project files (.pbxproj). 10-28x faster than alternatives (xcode, XcodeProj, xcodeproj) with better error messages and full spec compliance.
bun add @bacons/xcode
Run benchmarks with bun run bench or bun run bench:compare for cross-language comparison.
| Parser | Language | Time (29KB) | Time (263KB) | Relative |
|---|---|---|---|---|
| @bacons/xcode | TypeScript | 0.15ms | 0.81ms | 1x |
| xcode (legacy) | JavaScript | 1.54ms | crashes | 10x slower |
| XcodeProj (Tuist) | Swift | 2.00ms | 11.2ms | 13x slower |
| xcodeproj (CocoaPods) | Ruby | 3.63ms | 22.5ms | 24x slower |
Here is a diagram of the grammar used for parsing:
The most popular solution for parsing pbxproj files is a very old package by Cordova called xcode.
But xcode has some major issues:
Data type (<xx xx xx>).Consider the following format comparison.
Input .pbxproj
307D28A1123043350040C0FA /* app-icon.png */ = {
isa = PBXFileReference;
lastKnownFileType = image.png;
path = "app-icon.png";
sourceTree = "<group>";
};
xcode output (old)
{
"307D28A1123043350040C0FA_comment": "app-icon.png",
"308D052E1370CCF300D202BF": {
"isa": "PBXFileReference",
"lastKnownFileType": "image.png",
"path": "\"app-icon.png\"",
"sourceTree": "\"<group>\""
}
}
That same object would look like this in @bacons/xcode:
@bacons/xcode output (NEW)
{
"308D052E1370CCF300D202BF": {
"isa": "PBXFileReference",
"lastKnownFileType": "image.png",
"path": "app-icon.png",
"sourceTree": "<group>"
}
}
Notice how you don't need to strip or reapply quotes, you also don't need to filter out comments because the default visitor ignores comments in favor of regenerating them dynamically like Xcode does.
There's a mutable-graph layer which makes it much easier to work with pbxproj.
import {
PBXAggregateTarget,
PBXFrameworksBuildPhase,
PBXLegacyTarget,
PBXNativeTarget,
XcodeProject,
} from "@bacons/xcode";
const project = XcodeProject.open("/path/to/project.pbxproj");
// Get all targets:
project.rootObject.props.targets;
Create a Swift file:
import { PBXBuildFile, PBXFileReference } from "@bacons/xcode";
import path from "path";
// Get `project` from XcodeProject.
const file = PBXBuildFile.create(project, {
fileRef: PBXFileReference.create(project, {
path: "MyFile.swift",
sourceTree: "<group>",
}),
});
// The file and fileRef will now be injected in the pbxproj `objects` dict.
Parse and manipulate Xcode scheme files (.xcscheme). Schemes define how targets are built, run, tested, profiled, analyzed, and archived.
import * as scheme from "@bacons/xcode/scheme";
import fs from "fs";
// Parse an xcscheme file
const xml = fs.readFileSync(
"/path/to/Project.xcodeproj/xcshareddata/xcschemes/App.xcscheme",
"utf-8",
);
const xcscheme = scheme.parse(xml);
// Modify the scheme
xcscheme.buildAction.parallelizeBuildables = true;
xcscheme.launchAction.environmentVariables = [
{ key: "DEBUG", value: "1", isEnabled: true },
];
// Serialize back to XML
const outputXml = scheme.build(xcscheme);
fs.writeFileSync("/path/to/App.xcscheme", outputXml);
import { XcodeProject, XCScheme } from "@bacons/xcode";
// Load project and get schemes
const project = XcodeProject.open("/path/to/project.pbxproj");
const schemes = project.getSchemes();
const appScheme = project.getScheme("App");
// Create a new scheme
const newScheme = XCScheme.create("MyNewScheme");
newScheme.addBuildTarget({
buildableIdentifier: "primary",
blueprintIdentifier: "ABC123", // Target UUID
buildableName: "App.app",
blueprintName: "App",
referencedContainer: "container:Project.xcodeproj",
});
newScheme.setLaunchEnvironmentVariable("API_URL", "https://api.example.com");
newScheme.addLaunchArgument("-verbose");
// Save to disk
project.saveScheme(newScheme);
// Create scheme for an existing target
const mainTarget = project.rootObject.props.targets[0];
const targetScheme = project.createSchemeForTarget(mainTarget);
project.saveScheme(targetScheme);
Parse and build xcschememanagement.plist files that control scheme visibility and ordering:
import * as scheme from "@bacons/xcode/scheme";
import fs from "fs";
const plist = fs.readFileSync("/path/to/xcschememanagement.plist", "utf-8");
const management = scheme.parseManagement(plist);
// Check scheme visibility
console.log(management.SchemeUserState?.["App.xcscheme"]?.isShown);
// Rebuild plist
const output = scheme.buildManagement(management);
Parse and manipulate Xcode workspace files (.xcworkspace). Workspaces group multiple Xcode projects together, commonly used with CocoaPods, monorepos, and multi-project setups.
import * as workspace from "@bacons/xcode/workspace";
import fs from "fs";
// Parse a workspace file
const xml = fs.readFileSync(
"/path/to/MyApp.xcworkspace/contents.xcworkspacedata",
"utf-8",
);
const ws = workspace.parse(xml);
// Access project references
console.log(ws.fileRefs); // [{ location: "group:App.xcodeproj" }, ...]
// Modify the workspace
ws.fileRefs?.push({ location: "group:NewProject.xcodeproj" });
// Serialize back to XML
const outputXml = workspace.build(ws);
fs.writeFileSync("/path/to/contents.xcworkspacedata", outputXml);
import { XCWorkspace } from "@bacons/xcode";
// Open an existing workspace
const workspace = XCWorkspace.open("/path/to/MyApp.xcworkspace");
// Get all project paths
const projects = workspace.getProjectPaths();
console.log(projects); // ["group:App.xcodeproj", "group:Pods/Pods.xcodeproj"]
// Check if workspace contains a project
if (!workspace.hasProject("NewProject.xcodeproj")) {
workspace.addProject("NewProject.xcodeproj");
}
// Remove a project
workspace.removeProject("OldProject.xcodeproj");
// Work with groups
const group = workspace.addGroup("Libraries", "group:Libraries");
group.fileRefs?.push({ location: "group:Libraries/MyLib.xcodeproj" });
// Save changes
workspace.save();
// Create a new workspace
const newWorkspace = XCWorkspace.create("MyWorkspace");
newWorkspace.addProject("App.xcodeproj");
newWorkspace.addProject("Pods/Pods.xcodeproj");
newWorkspace.save("/path/to/MyWorkspace.xcworkspace");
Workspace file references use location specifiers:
group:path - Relative path from workspace root (most common)self: - Self-reference (embedded workspace inside .xcodeproj)container:path - Absolute container reference (rare)absolute:path - Absolute file pathManage IDEWorkspaceChecks.plist files that store workspace check states. Introduced in Xcode 9.3, these files prevent Xcode from recomputing checks each time a workspace is opened.
The primary use is suppressing the macOS 32-bit deprecation warning:
import { XCWorkspace, IDEWorkspaceChecks } from "@bacons/xcode";
// Suppress the 32-bit deprecation warning
const workspace = XCWorkspace.open("/path/to/MyApp.xcworkspace");
workspace.setMac32BitWarningComputed();
// Or work with IDEWorkspaceChecks directly
const checks = IDEWorkspaceChecks.openOrCreate("/path/to/MyApp.xcworkspace");
checks.mac32BitWarningComputed = true;
checks.save();
// Low-level API
import * as workspace from "@bacons/xcode/workspace";
const plist = workspace.parseChecks(plistString);
console.log(plist.IDEDidComputeMac32BitWarning); // true
const output = workspace.buildChecks({ IDEDidComputeMac32BitWarning: true });
Parse and manipulate Xcode configuration files (.xcconfig). These files define build settings that can be shared across targets and configurations.
import * as xcconfig from "@bacons/xcode/xcconfig";
import fs from "fs";
// Parse an xcconfig string
const config = xcconfig.parse(`
#include "Base.xcconfig"
PRODUCT_NAME = MyApp
OTHER_LDFLAGS[sdk=iphoneos*] = -framework UIKit
`);
// Parse from file (resolves #include directives)
const config = xcconfig.parseFile("/path/to/Project.xcconfig");
// Flatten build settings (merges includes, applies conditions)
const allSettings = xcconfig.flattenBuildSettings(config);
// Filter by platform conditions
const iosSettings = xcconfig.flattenBuildSettings(config, {
sdk: "iphoneos",
arch: "arm64",
config: "Release",
});
// Serialize back to xcconfig format
const output = xcconfig.build(config);
fs.writeFileSync("/path/to/Project.xcconfig", output);
XCConfig supports conditional settings based on SDK, architecture, and configuration:
// SDK-specific settings
OTHER_LDFLAGS[sdk=iphoneos*] = -framework UIKit
OTHER_LDFLAGS[sdk=macosx*] = -framework AppKit
// Architecture-specific settings
ARCHS[arch=arm64] = arm64
ARCHS[arch=x86_64] = x86_64
// Configuration-specific settings
GCC_OPTIMIZATION_LEVEL[config=Debug] = 0
GCC_OPTIMIZATION_LEVEL[config=Release] = s
// Combined conditions
LIBRARY_SEARCH_PATHS[sdk=iphoneos*][arch=arm64] = /usr/lib/arm64
// Required include - throws if file not found
#include "Base.xcconfig"
// Optional include - silently ignored if file not found
#include? "Optional.xcconfig"
XCConfig supports variable references and the $(inherited) keyword:
// Reference other settings
PRODUCT_BUNDLE_IDENTIFIER = $(BUNDLE_ID_PREFIX).$(PRODUCT_NAME:lower)
// Inherit from included files
OTHER_LDFLAGS = $(inherited) -framework UIKit
Access and manipulate shared data directories (xcshareddata) which contain schemes, breakpoints, and workspace settings that are intended for version control.
import { XcodeProject, XCSharedData } from "@bacons/xcode";
// Get shared data from a project
const project = XcodeProject.open("/path/to/project.pbxproj");
const sharedData = project.getSharedData();
// Access schemes
const schemes = sharedData.getSchemes();
const appScheme = sharedData.getScheme("App");
// Access breakpoints
if (sharedData.breakpoints) {
console.log(sharedData.breakpoints.breakpoints?.length);
}
// Access workspace settings
if (sharedData.workspaceSettings) {
console.log(sharedData.workspaceSettings.PreviewsEnabled);
}
// Modify and save
sharedData.workspaceSettings = {
PreviewsEnabled: true,
IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded: false,
};
sharedData.save();
Parse and build Xcode breakpoint files (Breakpoints_v2.xcbkptlist):
import * as breakpoints from "@bacons/xcode/breakpoints";
import fs from "fs";
// Parse breakpoint file
const xml = fs.readFileSync(
"/path/to/xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist",
"utf-8",
);
const list = breakpoints.parse(xml);
// Access breakpoints
for (const bp of list.breakpoints ?? []) {
console.log(bp.breakpointExtensionID); // "Xcode.Breakpoint.FileBreakpoint"
console.log(bp.breakpointContent?.filePath);
console.log(bp.breakpointContent?.startingLineNumber);
}
// Add a new breakpoint
list.breakpoints?.push({
breakpointExtensionID: "Xcode.Breakpoint.FileBreakpoint",
breakpointContent: {
uuid: "new-uuid",
shouldBeEnabled: true,
filePath: "MyApp/ViewController.swift",
startingLineNumber: "42",
endingLineNumber: "42",
actions: [
{
actionExtensionID: "Xcode.BreakpointAction.DebuggerCommand",
actionContent: { consoleCommand: "po self" },
},
],
},
});
// Serialize back to XML
const outputXml = breakpoints.build(list);
Parse and build workspace settings files (WorkspaceSettings.xcsettings):
import * as settings from "@bacons/xcode/settings";
import fs from "fs";
// Parse settings file
const plist = fs.readFileSync(
"/path/to/xcshareddata/WorkspaceSettings.xcsettings",
"utf-8",
);
const config = settings.parse(plist);
console.log(config.BuildSystemType); // "Original" or "New"
console.log(config.PreviewsEnabled); // true/false
// Modify and save
config.PreviewsEnabled = true;
config.IDEWorkspaceSharedSettings_AutocreateContextsIfNeeded = false;
const outputPlist = settings.build(config);
fs.writeFileSync("/path/to/WorkspaceSettings.xcsettings", outputPlist);
Add Swift Package Manager dependencies to your Xcode projects with full wiring handled automatically.
import { XcodeProject } from "@bacons/xcode";
const project = XcodeProject.open("/path/to/project.pbxproj");
const rootProject = project.rootObject;
// Add a remote Swift package to the project
const packageRef = rootProject.addRemoteSwiftPackage({
repositoryURL: "https://github.com/apple/swift-collections",
requirement: {
kind: "upToNextMajorVersion",
minimumVersion: "1.0.0",
},
});
// Add the package product to a target
const target = rootProject.getMainAppTarget("ios");
const productDep = target.addSwiftPackageProduct({
productName: "Collections",
package: packageRef,
});
// Save the project
fs.writeFileSync("/path/to/project.pbxproj", build(project.toJSON()));
// Add a local Swift package (e.g., from a monorepo)
const localPackage = rootProject.addLocalSwiftPackage({
relativePath: "../Packages/MyFeature",
});
// Add the product to your target
target.addSwiftPackageProduct({
productName: "MyFeature",
package: localPackage,
});
When you call target.addSwiftPackageProduct(), the following is handled automatically:
XCSwiftPackageProductDependency and adds it to target's packageProductDependenciesPBXBuildFile with productRef pointing to the dependency// Get all package product dependencies for a target
const deps = target.getSwiftPackageProductDependencies();
// Find an existing package by URL or path
const existing = rootProject.getPackageReference(
"https://github.com/apple/swift-collections"
);
// Remove a package product from a target (cleans up build file too)
target.removeSwiftPackageProduct(productDep);
Remote packages support various version requirement types:
// Up to next major version (e.g., 1.0.0 to 2.0.0)
{ kind: "upToNextMajorVersion", minimumVersion: "1.0.0" }
// Up to next minor version (e.g., 1.2.0 to 1.3.0)
{ kind: "upToNextMinorVersion", minimumVersion: "1.2.0" }
// Exact version
{ kind: "exactVersion", version: "1.2.3" }
// Version range
{ kind: "versionRange", minimumVersion: "1.0.0", maximumVersion: "2.0.0" }
// Branch
{ kind: "branch", branch: "main" }
// Revision (commit hash)
{ kind: "revision", revision: "abc123def456" }
xcode package (which uses PEG.js).<xx xx xx>.The parsing is very simple (simplicity is the key).
pbxproj is an "old-style plist" (or ASCII Plist), this means it should be possible to represent it as any other static configuration file type like JSON or XML.
We support the following types: Object, Array, Data, String. Notably, we avoid dealing with Integer, Double, Boolean since they appear to not exist in the format.
bun run bench)..xcconfig file parsing with #include support and build settings flattening.xcshareddata/WorkspaceSettings.xcsettings) Derived data location, build system version, auto-create schemes setting.xcshareddata/xcdebugger/Breakpoints_v2.xcbkptlist) Shared debugger breakpoints (file, symbolic, exception breakpoints).xcuserdata/<user>.xcuserdatad/) Per-user schemes, breakpoints, UI state.xcshareddata/IDEWorkspaceChecks.plist) Workspace check state storage (e.g., 32-bit deprecation warning).Docs are in the works. For now, you can refer to the types and the estimated pbxproj spec.
The API will change in the future, for now we have two methods:
import {
/** Given a stringified `pbxproj`, return a JSON representation of the object. */
parse,
/** Given a JSON representation of a `pbxproj`, return a `.pbxproj` string that can be parsed by Xcode. */
build,
} from "@bacons/xcode/json";
import fs from "fs";
import path from "path";
const pbxproj = parse(fs.readFileSync("/path/to/project.pbxproj"));
const pbxprojString = build(pbxproj);
PBXVariantGroup is a localized PBXGroup.Files will have an attribute sourceTree which indicates how the file path should be resolved.
BUILT_PRODUCTS_DIR: Paths are relative to the built products directory.DEVELOPER_DIR: Paths are relative to the developer directory.SOURCE_ROOT: Paths are relative to the project.SDKROOT: Paths are relative to the SDK directory.<group>: Paths are relative to the group.<absolute>: Source is an absolute path.For example, a file object like:
{
"isa": "PBXFileReference",
"name": "AppDelegate.m",
"path": "multitarget/AppDelegate.m",
"sourceTree": "<group>"
}
Indicates that the path "multitarget/AppDelegate.m" is relative to sourceTree "". We need to check the containing PBXGroup's path (only defined when the group is linked to a directory in the file system). Groups can live inside of other groups so this process is recursive.
Certain values loosely map to each other. For instance the top-level objectVersion (which indicates the versioning used for the objects in the top-level objects dictionary), maps to the rootObject -> PBXProject's compatibilityVersion string. Here is an up-to-date mapping (May 2022):
PBXProject.compatibilityVersion | XcodeProject.objectVersion |
|---|---|
'Xcode 16.0' | 70 |
'Xcode 15.0' | 60 |
'Xcode 14.0' | 56 |
'Xcode 13.0' | 55 |
'Xcode 12.0' | 54 |
'Xcode 11.4' | 53 |
'Xcode 11.0' | 52 |
'Xcode 10.0' | 51 |
'Xcode 9.3' | 50 |
'Xcode 8.0' | 48 |
'Xcode 6.3' | 47 |
'Xcode 3.2' | 46 |
'Xcode 3.1' | 45 |
FAQs
pbxproj parser
The npm package @bacons/xcode receives a total of 280,451 weekly downloads. As such, @bacons/xcode popularity was classified as popular.
We found that @bacons/xcode demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
/Security News
A mini Shai-Hulud campaign compromised Red Hat Cloud Services npm packages to steal developer and CI/CD secrets during installation.

Research
/Security News
The North Korean malware loader hides in a Packagist-listed package and its GitHub branch to fetch and execute remote code in a likely Contagious Interview-style lure.

Security News
The Rust project is moving toward formal rules on LLM use in contributions after months of internal debate over maintainer burden, code quality, and contributor experience.