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; +}