From 4dfb341cf11aba499bcda6728619bda281f40fdc Mon Sep 17 00:00:00 2001 From: evanbacon Date: Fri, 27 Feb 2026 12:40:20 -0800 Subject: [PATCH] Add IDEWorkspaceChecks support for workspace check state Add support for parsing and building IDEWorkspaceChecks.plist files. Introduced in Xcode 9.3, these store workspace check states to prevent recomputation on each open. Primary use: suppressing the macOS 32-bit deprecation warning via IDEDidComputeMac32BitWarning flag. Features: - Low-level API: parseChecks/buildChecks for plist manipulation - High-level API: IDEWorkspaceChecks class with open/create/save methods - XCWorkspace integration: getWorkspaceChecks, setMac32BitWarningComputed - Convenience property: mac32BitWarningComputed getter/setter Co-Authored-By: Claude Opus 4.5 --- README.md | 29 +- src/api/IDEWorkspaceChecks.ts | 176 +++++++ src/api/XCWorkspace.ts | 51 ++ src/api/__tests__/IDEWorkspaceChecks.test.ts | 461 ++++++++++++++++++ src/api/index.ts | 1 + src/workspace/__tests__/checks.test.ts | 145 ++++++ .../fixtures/IDEWorkspaceChecks.plist | 8 + src/workspace/checks.ts | 42 ++ src/workspace/index.ts | 1 + src/workspace/types.ts | 19 + 10 files changed, 932 insertions(+), 1 deletion(-) create mode 100644 src/api/IDEWorkspaceChecks.ts create mode 100644 src/api/__tests__/IDEWorkspaceChecks.test.ts create mode 100644 src/workspace/__tests__/checks.test.ts create mode 100644 src/workspace/__tests__/fixtures/IDEWorkspaceChecks.plist create mode 100644 src/workspace/checks.ts diff --git a/README.md b/README.md index 387151c..9db6f5d 100644 --- a/README.md +++ b/README.md @@ -277,6 +277,33 @@ Workspace file references use location specifiers: - `container:path` - Absolute container reference (rare) - `absolute:path` - Absolute file path +### IDEWorkspaceChecks + +Manage `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: + +```ts +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 }); +``` + ## XCConfig Support Parse and manipulate Xcode configuration files (`.xcconfig`). These files define build settings that can be shared across targets and configurations. @@ -496,7 +523,7 @@ We support the following types: `Object`, `Array`, `Data`, `String`. Notably, we - [ ] Skills. - [ ] Import from other tools. - [ ] **XCUserData**: (`xcuserdata/.xcuserdatad/`) Per-user schemes, breakpoints, UI state. -- [ ] **IDEWorkspaceChecks**: (`xcshareddata/IDEWorkspaceChecks.plist`) "Trust this project" flag that suppresses Xcode warning. +- [x] **IDEWorkspaceChecks**: (`xcshareddata/IDEWorkspaceChecks.plist`) Workspace check state storage (e.g., 32-bit deprecation warning). # Docs diff --git a/src/api/IDEWorkspaceChecks.ts b/src/api/IDEWorkspaceChecks.ts new file mode 100644 index 0000000..9993e2b --- /dev/null +++ b/src/api/IDEWorkspaceChecks.ts @@ -0,0 +1,176 @@ +/** + * High-level API for IDEWorkspaceChecks.plist files. + * + * Introduced in Xcode 9.3, these files store the state of workspace checks + * to prevent them from being recomputed each time the workspace is opened. + * + * Currently known keys: + * - IDEDidComputeMac32BitWarning: Tracks whether the 32-bit macOS deprecation + * warning has been computed/shown. Setting to true suppresses the warning. + */ +import { readFileSync, writeFileSync, existsSync, mkdirSync } from "fs"; +import path from "path"; + +import { parseChecks, buildChecks } from "../workspace/checks"; +import type { IDEWorkspaceChecks as ChecksData } from "../workspace/types"; + +/** Relative path from workspace to the checks plist */ +const CHECKS_PATH = "xcshareddata/IDEWorkspaceChecks.plist"; + +/** + * High-level class for working with IDEWorkspaceChecks.plist files. + */ +export class IDEWorkspaceChecks { + /** The parsed checks data */ + props: ChecksData; + + /** Path to the plist file (may be undefined for new instances) */ + filePath?: string; + + private constructor(props: ChecksData, filePath?: string) { + this.props = props; + this.filePath = filePath; + } + + /** + * Open an existing IDEWorkspaceChecks.plist from a workspace. + * + * @param workspacePath Path to the .xcworkspace directory + * @returns The checks instance, or null if the file doesn't exist + */ + static open(workspacePath: string): IDEWorkspaceChecks | null { + const checksPath = path.join(workspacePath, CHECKS_PATH); + if (!existsSync(checksPath)) { + return null; + } + + const plistString = readFileSync(checksPath, "utf-8"); + const props = parseChecks(plistString); + return new IDEWorkspaceChecks(props, checksPath); + } + + /** + * Open an existing IDEWorkspaceChecks.plist or create a new one. + * + * @param workspacePath Path to the .xcworkspace directory + * @returns The checks instance (opened or newly created) + */ + static openOrCreate(workspacePath: string): IDEWorkspaceChecks { + const existing = IDEWorkspaceChecks.open(workspacePath); + if (existing) { + return existing; + } + + const checksPath = path.join(workspacePath, CHECKS_PATH); + return new IDEWorkspaceChecks( + { IDEDidComputeMac32BitWarning: true }, + checksPath + ); + } + + /** + * Create a new IDEWorkspaceChecks instance. + * + * @param options Optional initial props and file path + */ + static create(options?: { + props?: Partial; + filePath?: string; + }): IDEWorkspaceChecks { + const defaultProps: ChecksData = { + IDEDidComputeMac32BitWarning: true, + }; + + const props = { ...defaultProps, ...options?.props }; + return new IDEWorkspaceChecks(props, options?.filePath); + } + + /** + * Save the checks to disk. + * + * @param filePath Optional path to save to. If not provided, uses this.filePath. + */ + save(filePath?: string): void { + const targetPath = filePath ?? this.filePath; + if (!targetPath) { + throw new Error( + "No file path specified. Either provide a path or set this.filePath." + ); + } + + // Ensure parent directory exists + const dir = path.dirname(targetPath); + if (!existsSync(dir)) { + mkdirSync(dir, { recursive: true }); + } + + const plistString = buildChecks(this.props); + writeFileSync(targetPath, plistString, "utf-8"); + this.filePath = targetPath; + } + + /** + * Save the checks to a workspace directory. + * + * @param workspacePath Path to the .xcworkspace directory + */ + saveToWorkspace(workspacePath: string): void { + const checksPath = path.join(workspacePath, CHECKS_PATH); + this.save(checksPath); + } + + /** + * Get the plist representation of the checks. + */ + toPlist(): string { + return buildChecks(this.props); + } + + /** + * Get whether the Mac 32-bit warning has been computed/dismissed. + */ + get mac32BitWarningComputed(): boolean { + return this.props.IDEDidComputeMac32BitWarning ?? false; + } + + /** + * Set whether the Mac 32-bit warning has been computed/dismissed. + */ + set mac32BitWarningComputed(value: boolean) { + this.props.IDEDidComputeMac32BitWarning = value; + } + + /** + * Get a check value by key. + * + * @param key The check key + * @returns The boolean value, or undefined if not set + */ + getCheck(key: string): boolean | undefined { + return this.props[key]; + } + + /** + * Set a check value. + * + * @param key The check key + * @param value The boolean value + */ + setCheck(key: string, value: boolean): void { + this.props[key] = value; + } + + /** + * Remove a check. + * + * @param key The check key + * @returns true if the check was removed, false if it didn't exist + */ + removeCheck(key: string): boolean { + if (key in this.props) { + delete this.props[key]; + return true; + } + return false; + } +} diff --git a/src/api/XCWorkspace.ts b/src/api/XCWorkspace.ts index be471f3..0b5a5b2 100644 --- a/src/api/XCWorkspace.ts +++ b/src/api/XCWorkspace.ts @@ -8,6 +8,7 @@ import path from "path"; import * as workspace from "../workspace"; import type { XCWorkspace as WorkspaceData, FileRef, Group } from "../workspace/types"; +import { IDEWorkspaceChecks } from "./IDEWorkspaceChecks"; import { XCSharedData } from "./XCSharedData"; /** @@ -236,6 +237,56 @@ export class XCWorkspace { }); } + /** + * Get the IDEWorkspaceChecks for this workspace. + * + * @returns The checks instance, or null if not set + */ + getWorkspaceChecks(): IDEWorkspaceChecks | null { + if (!this.filePath) { + return null; + } + return IDEWorkspaceChecks.open(this.filePath); + } + + /** + * Get or create the IDEWorkspaceChecks for this workspace. + * + * @returns The checks instance (opened or newly created) + */ + getOrCreateWorkspaceChecks(): IDEWorkspaceChecks { + if (!this.filePath) { + throw new Error( + "Workspace must be saved before accessing workspace checks." + ); + } + return IDEWorkspaceChecks.openOrCreate(this.filePath); + } + + /** + * Check if this workspace has IDEWorkspaceChecks configured. + * + * @returns true if the checks plist exists + */ + hasWorkspaceChecks(): boolean { + if (!this.filePath) { + return false; + } + return IDEWorkspaceChecks.open(this.filePath) !== null; + } + + /** + * Mark the 32-bit warning as computed. + * + * This suppresses the macOS 32-bit deprecation warning dialog in Xcode + * by setting IDEDidComputeMac32BitWarning to true. + */ + setMac32BitWarningComputed(): void { + const checks = this.getOrCreateWorkspaceChecks(); + checks.mac32BitWarningComputed = true; + checks.save(); + } + private collectPathsFromGroup(group: Group): string[] { const paths: string[] = []; diff --git a/src/api/__tests__/IDEWorkspaceChecks.test.ts b/src/api/__tests__/IDEWorkspaceChecks.test.ts new file mode 100644 index 0000000..74bd4d1 --- /dev/null +++ b/src/api/__tests__/IDEWorkspaceChecks.test.ts @@ -0,0 +1,461 @@ +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "fs"; +import path from "path"; +import tempy from "tempy"; + +import { IDEWorkspaceChecks } from "../IDEWorkspaceChecks"; +import { XCWorkspace } from "../XCWorkspace"; + +describe("IDEWorkspaceChecks", () => { + describe("create", () => { + it("creates with default values", () => { + const checks = IDEWorkspaceChecks.create(); + + expect(checks.props.IDEDidComputeMac32BitWarning).toBe(true); + expect(checks.filePath).toBeUndefined(); + }); + + it("creates with custom props", () => { + const checks = IDEWorkspaceChecks.create({ + props: { + IDEDidComputeMac32BitWarning: false, + SomeOtherFlag: true, + }, + }); + + expect(checks.props.IDEDidComputeMac32BitWarning).toBe(false); + expect(checks.props.SomeOtherFlag).toBe(true); + }); + + it("creates with file path", () => { + const checks = IDEWorkspaceChecks.create({ + filePath: "/path/to/checks.plist", + }); + + expect(checks.filePath).toBe("/path/to/checks.plist"); + }); + }); + + describe("open", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = tempy.directory(); + }); + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true }); + } + }); + + it("opens existing checks from workspace", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + const sharedDataPath = path.join(workspacePath, "xcshareddata"); + mkdirSync(sharedDataPath, { recursive: true }); + writeFileSync( + path.join(sharedDataPath, "IDEWorkspaceChecks.plist"), + ` + + + + IDEDidComputeMac32BitWarning + + +` + ); + + const checks = IDEWorkspaceChecks.open(workspacePath); + + expect(checks).not.toBeNull(); + expect(checks!.props.IDEDidComputeMac32BitWarning).toBe(true); + expect(checks!.filePath).toBe( + path.join(sharedDataPath, "IDEWorkspaceChecks.plist") + ); + }); + + it("returns null when file does not exist", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + mkdirSync(workspacePath, { recursive: true }); + + const checks = IDEWorkspaceChecks.open(workspacePath); + + expect(checks).toBeNull(); + }); + }); + + describe("openOrCreate", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = tempy.directory(); + }); + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true }); + } + }); + + it("opens existing checks", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + const sharedDataPath = path.join(workspacePath, "xcshareddata"); + mkdirSync(sharedDataPath, { recursive: true }); + writeFileSync( + path.join(sharedDataPath, "IDEWorkspaceChecks.plist"), + ` + + + + IDEDidComputeMac32BitWarning + + +` + ); + + const checks = IDEWorkspaceChecks.openOrCreate(workspacePath); + + expect(checks.props.IDEDidComputeMac32BitWarning).toBe(false); + }); + + it("creates new checks when file does not exist", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + mkdirSync(workspacePath, { recursive: true }); + + const checks = IDEWorkspaceChecks.openOrCreate(workspacePath); + + expect(checks.props.IDEDidComputeMac32BitWarning).toBe(true); + expect(checks.filePath).toBe( + path.join(workspacePath, "xcshareddata/IDEWorkspaceChecks.plist") + ); + }); + }); + + describe("save", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = tempy.directory(); + }); + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true }); + } + }); + + it("saves to specified path", () => { + const checks = IDEWorkspaceChecks.create(); + const filePath = path.join(tempDir, "checks.plist"); + + checks.save(filePath); + + expect(existsSync(filePath)).toBe(true); + const content = readFileSync(filePath, "utf-8"); + expect(content).toContain("IDEDidComputeMac32BitWarning"); + }); + + it("creates xcshareddata directory if needed", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + mkdirSync(workspacePath, { recursive: true }); + + const checks = IDEWorkspaceChecks.create(); + const filePath = path.join( + workspacePath, + "xcshareddata/IDEWorkspaceChecks.plist" + ); + + checks.save(filePath); + + expect(existsSync(filePath)).toBe(true); + }); + + it("uses filePath when no argument provided", () => { + const filePath = path.join(tempDir, "checks.plist"); + const checks = IDEWorkspaceChecks.create({ filePath }); + + checks.save(); + + expect(existsSync(filePath)).toBe(true); + }); + + it("throws when no path specified", () => { + const checks = IDEWorkspaceChecks.create(); + + expect(() => checks.save()).toThrow("No file path specified"); + }); + }); + + describe("saveToWorkspace", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = tempy.directory(); + }); + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true }); + } + }); + + it("saves to workspace path", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + mkdirSync(workspacePath, { recursive: true }); + + const checks = IDEWorkspaceChecks.create(); + checks.saveToWorkspace(workspacePath); + + const expectedPath = path.join( + workspacePath, + "xcshareddata/IDEWorkspaceChecks.plist" + ); + expect(existsSync(expectedPath)).toBe(true); + }); + }); + + describe("toPlist", () => { + it("returns plist string", () => { + const checks = IDEWorkspaceChecks.create(); + + const plist = checks.toPlist(); + + expect(plist).toContain(""); + }); + }); + + describe("mac32BitWarningComputed", () => { + it("gets value", () => { + const checks = IDEWorkspaceChecks.create({ + props: { IDEDidComputeMac32BitWarning: true }, + }); + + expect(checks.mac32BitWarningComputed).toBe(true); + }); + + it("defaults to true when props is empty", () => { + const checks = IDEWorkspaceChecks.create({ + props: {}, + }); + + // create() merges with defaults, so IDEDidComputeMac32BitWarning is always true + expect(checks.mac32BitWarningComputed).toBe(true); + }); + + it("sets value", () => { + const checks = IDEWorkspaceChecks.create({ + props: { IDEDidComputeMac32BitWarning: true }, + }); + + checks.mac32BitWarningComputed = false; + + expect(checks.props.IDEDidComputeMac32BitWarning).toBe(false); + }); + }); + + describe("getCheck/setCheck/removeCheck", () => { + it("gets check value", () => { + const checks = IDEWorkspaceChecks.create({ + props: { CustomFlag: true }, + }); + + expect(checks.getCheck("CustomFlag")).toBe(true); + }); + + it("returns undefined for missing check", () => { + const checks = IDEWorkspaceChecks.create(); + + expect(checks.getCheck("NonexistentFlag")).toBeUndefined(); + }); + + it("sets check value", () => { + const checks = IDEWorkspaceChecks.create(); + + checks.setCheck("CustomFlag", true); + + expect(checks.props.CustomFlag).toBe(true); + }); + + it("removes check", () => { + const checks = IDEWorkspaceChecks.create({ + props: { CustomFlag: true }, + }); + + const result = checks.removeCheck("CustomFlag"); + + expect(result).toBe(true); + expect(checks.props.CustomFlag).toBeUndefined(); + }); + + it("returns false when removing non-existent check", () => { + const checks = IDEWorkspaceChecks.create(); + + const result = checks.removeCheck("NonexistentFlag"); + + expect(result).toBe(false); + }); + }); +}); + +describe("XCWorkspace integration", () => { + let tempDir: string; + + beforeEach(() => { + tempDir = tempy.directory(); + }); + + afterEach(() => { + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true }); + } + }); + + describe("getWorkspaceChecks", () => { + it("returns null when no file path", () => { + const workspace = XCWorkspace.create("Test"); + + expect(workspace.getWorkspaceChecks()).toBeNull(); + }); + + it("returns null when checks do not exist", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + mkdirSync(workspacePath, { recursive: true }); + writeFileSync( + path.join(workspacePath, "contents.xcworkspacedata"), + ` +` + ); + + const workspace = XCWorkspace.open(workspacePath); + + expect(workspace.getWorkspaceChecks()).toBeNull(); + }); + + it("returns checks when they exist", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + const sharedDataPath = path.join(workspacePath, "xcshareddata"); + mkdirSync(sharedDataPath, { recursive: true }); + writeFileSync( + path.join(workspacePath, "contents.xcworkspacedata"), + ` +` + ); + writeFileSync( + path.join(sharedDataPath, "IDEWorkspaceChecks.plist"), + ` + + + + IDEDidComputeMac32BitWarning + + +` + ); + + const workspace = XCWorkspace.open(workspacePath); + const checks = workspace.getWorkspaceChecks(); + + expect(checks).not.toBeNull(); + expect(checks!.mac32BitWarningComputed).toBe(true); + }); + }); + + describe("getOrCreateWorkspaceChecks", () => { + it("throws when workspace has no file path", () => { + const workspace = XCWorkspace.create("Test"); + + expect(() => workspace.getOrCreateWorkspaceChecks()).toThrow( + "Workspace must be saved before accessing workspace checks" + ); + }); + + it("creates checks when they do not exist", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + mkdirSync(workspacePath, { recursive: true }); + writeFileSync( + path.join(workspacePath, "contents.xcworkspacedata"), + ` +` + ); + + const workspace = XCWorkspace.open(workspacePath); + const checks = workspace.getOrCreateWorkspaceChecks(); + + expect(checks.mac32BitWarningComputed).toBe(true); + }); + }); + + describe("hasWorkspaceChecks", () => { + it("returns false when no file path", () => { + const workspace = XCWorkspace.create("Test"); + + expect(workspace.hasWorkspaceChecks()).toBe(false); + }); + + it("returns false when checks do not exist", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + mkdirSync(workspacePath, { recursive: true }); + writeFileSync( + path.join(workspacePath, "contents.xcworkspacedata"), + ` +` + ); + + const workspace = XCWorkspace.open(workspacePath); + + expect(workspace.hasWorkspaceChecks()).toBe(false); + }); + + it("returns true when checks exist", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + const sharedDataPath = path.join(workspacePath, "xcshareddata"); + mkdirSync(sharedDataPath, { recursive: true }); + writeFileSync( + path.join(workspacePath, "contents.xcworkspacedata"), + ` +` + ); + writeFileSync( + path.join(sharedDataPath, "IDEWorkspaceChecks.plist"), + ` + + + + IDEDidComputeMac32BitWarning + + +` + ); + + const workspace = XCWorkspace.open(workspacePath); + + expect(workspace.hasWorkspaceChecks()).toBe(true); + }); + }); + + describe("setMac32BitWarningComputed", () => { + it("creates checks file with warning computed flag", () => { + const workspacePath = path.join(tempDir, "Test.xcworkspace"); + mkdirSync(workspacePath, { recursive: true }); + writeFileSync( + path.join(workspacePath, "contents.xcworkspacedata"), + ` +` + ); + + const workspace = XCWorkspace.open(workspacePath); + workspace.setMac32BitWarningComputed(); + + const checksPath = path.join( + workspacePath, + "xcshareddata/IDEWorkspaceChecks.plist" + ); + expect(existsSync(checksPath)).toBe(true); + + const content = readFileSync(checksPath, "utf-8"); + expect(content).toContain("IDEDidComputeMac32BitWarning"); + expect(content).toContain(""); + }); + }); +}); diff --git a/src/api/index.ts b/src/api/index.ts index 80bbdb8..ffbc466 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -36,3 +36,4 @@ export { XCVersionGroup } from "./XCVersionGroup"; export { XCScheme, createBuildableReference } from "./XCScheme"; export { XCSharedData } from "./XCSharedData"; export { XCWorkspace } from "./XCWorkspace"; +export { IDEWorkspaceChecks } from "./IDEWorkspaceChecks"; diff --git a/src/workspace/__tests__/checks.test.ts b/src/workspace/__tests__/checks.test.ts new file mode 100644 index 0000000..d173d9a --- /dev/null +++ b/src/workspace/__tests__/checks.test.ts @@ -0,0 +1,145 @@ +import { readFileSync } from "fs"; +import path from "path"; + +import { parseChecks, buildChecks } from "../checks"; +import type { IDEWorkspaceChecks } from "../types"; + +const fixturesDir = path.join(__dirname, "fixtures"); + +function readFixture(name: string): string { + return readFileSync(path.join(fixturesDir, name), "utf-8"); +} + +describe("checks parser", () => { + describe("parseChecks", () => { + it("parses basic IDEWorkspaceChecks.plist", () => { + const plistString = readFixture("IDEWorkspaceChecks.plist"); + const checks = parseChecks(plistString); + + expect(checks.IDEDidComputeMac32BitWarning).toBe(true); + }); + + it("parses plist with multiple flags", () => { + const plistString = ` + + + + IDEDidComputeMac32BitWarning + + SomeOtherFlag + + +`; + + const checks = parseChecks(plistString); + + expect(checks.IDEDidComputeMac32BitWarning).toBe(true); + expect(checks.SomeOtherFlag).toBe(false); + }); + + it("parses empty plist", () => { + const plistString = ` + + + + +`; + + const checks = parseChecks(plistString); + + expect(Object.keys(checks)).toHaveLength(0); + }); + + it("ignores non-boolean values", () => { + const plistString = ` + + + + IDEDidComputeMac32BitWarning + + SomeString + hello + SomeNumber + 42 + +`; + + const checks = parseChecks(plistString); + + expect(checks.IDEDidComputeMac32BitWarning).toBe(true); + expect(checks.SomeString).toBeUndefined(); + expect(checks.SomeNumber).toBeUndefined(); + }); + }); + + describe("buildChecks", () => { + it("builds basic checks plist", () => { + const checks: IDEWorkspaceChecks = { + IDEDidComputeMac32BitWarning: true, + }; + + const plistString = buildChecks(checks); + + expect(plistString).toContain("IDEDidComputeMac32BitWarning"); + expect(plistString).toContain(""); + }); + + it("builds plist with multiple flags", () => { + const checks: IDEWorkspaceChecks = { + IDEDidComputeMac32BitWarning: true, + SomeOtherFlag: false, + }; + + const plistString = buildChecks(checks); + + expect(plistString).toContain("IDEDidComputeMac32BitWarning"); + expect(plistString).toContain(""); + expect(plistString).toContain("SomeOtherFlag"); + expect(plistString).toContain(""); + }); + + it("filters undefined values", () => { + const checks: IDEWorkspaceChecks = { + IDEDidComputeMac32BitWarning: true, + UndefinedFlag: undefined, + }; + + const plistString = buildChecks(checks); + + expect(plistString).toContain("IDEDidComputeMac32BitWarning"); + expect(plistString).not.toContain("UndefinedFlag"); + }); + + it("builds empty checks", () => { + const checks: IDEWorkspaceChecks = {}; + + const plistString = buildChecks(checks); + + expect(plistString).toContain(""); + }); + }); + + describe("round-trip", () => { + it("round-trips fixture data", () => { + const plistString = readFixture("IDEWorkspaceChecks.plist"); + const checks = parseChecks(plistString); + const rebuilt = buildChecks(checks); + const reparsed = parseChecks(rebuilt); + + expect(reparsed).toEqual(checks); + }); + + it("round-trips multiple flags", () => { + const original: IDEWorkspaceChecks = { + IDEDidComputeMac32BitWarning: true, + SomeOtherFlag: false, + AnotherFlag: true, + }; + + const plistString = buildChecks(original); + const reparsed = parseChecks(plistString); + + expect(reparsed).toEqual(original); + }); + }); +}); diff --git a/src/workspace/__tests__/fixtures/IDEWorkspaceChecks.plist b/src/workspace/__tests__/fixtures/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/src/workspace/__tests__/fixtures/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/src/workspace/checks.ts b/src/workspace/checks.ts new file mode 100644 index 0000000..cffb738 --- /dev/null +++ b/src/workspace/checks.ts @@ -0,0 +1,42 @@ +/** + * Parser and writer for IDEWorkspaceChecks.plist files. + * + * Introduced in Xcode 9.3, these files store the state of workspace checks + * to prevent them from being recomputed each time the workspace is opened. + * The primary use is suppressing the macOS 32-bit deprecation warning. + */ +import plist from "@expo/plist"; + +import type { IDEWorkspaceChecks } from "./types"; + +/** + * Parse an IDEWorkspaceChecks.plist string into a typed object. + */ +export function parseChecks(plistString: string): IDEWorkspaceChecks { + const parsed = plist.parse(plistString) as Record; + + const result: IDEWorkspaceChecks = {}; + + for (const [key, value] of Object.entries(parsed)) { + if (typeof value === "boolean") { + result[key] = value; + } + } + + return result; +} + +/** + * Build an IDEWorkspaceChecks.plist string from a typed object. + */ +export function buildChecks(checks: IDEWorkspaceChecks): string { + const obj: Record = {}; + + for (const [key, value] of Object.entries(checks)) { + if (typeof value === "boolean") { + obj[key] = value; + } + } + + return plist.build(obj); +} diff --git a/src/workspace/index.ts b/src/workspace/index.ts index e01b86f..8e37ab9 100644 --- a/src/workspace/index.ts +++ b/src/workspace/index.ts @@ -18,4 +18,5 @@ export { parse } from "./parser"; export { build } from "./writer"; +export { parseChecks, buildChecks } from "./checks"; export * from "./types"; diff --git a/src/workspace/types.ts b/src/workspace/types.ts index 0928b76..40ecbbc 100644 --- a/src/workspace/types.ts +++ b/src/workspace/types.ts @@ -43,3 +43,22 @@ export interface Group { /** Nested groups */ groups?: Group[]; } + +/** + * IDEWorkspaceChecks plist data. + * + * Introduced in Xcode 9.3, this file stores the state of workspace checks + * to prevent them from being recomputed each time the workspace is opened. + * Stored at `.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist` + * + * @see https://developer.apple.com/documentation/xcode-release-notes/xcode-9_3-release-notes + */ +export interface IDEWorkspaceChecks { + /** + * Whether the macOS 32-bit deprecation warning has been computed/shown. + * Setting to true suppresses the warning dialog. + */ + IDEDidComputeMac32BitWarning?: boolean; + /** Allow additional boolean flags for future Xcode versions */ + [key: string]: boolean | undefined; +}