UNPKG

@sentry/wizard

Version:

Sentry wizard helping you to configure your project

853 lines (852 loc) 92.5 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k); __setModuleDefault(result, mod); return result; }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); const fs = __importStar(require("node:fs")); const os = __importStar(require("node:os")); const path = __importStar(require("node:path")); const node_process_1 = __importDefault(require("node:process")); const vitest_1 = require("vitest"); const templates_1 = require("../../src/apple/templates"); const xcode_manager_1 = require("../../src/apple/xcode-manager"); vitest_1.vi.mock('node:fs', async () => ({ __esModule: true, ...(await vitest_1.vi.importActual('node:fs')), })); vitest_1.vi.mock('@clack/prompts', () => ({ log: { info: vitest_1.vi.fn(), success: vitest_1.vi.fn(), step: vitest_1.vi.fn(), }, })); const appleProjectsPath = path.resolve(__dirname, '../../fixtures/test-applications/apple'); const damagedProjectPath = path.join(appleProjectsPath, 'damaged-missing-configuration-list/Project.xcodeproj/project.pbxproj'); const noTargetsProjectPath = path.join(appleProjectsPath, 'no-targets/Project.xcodeproj/project.pbxproj'); const noFilesInTargetProjectPath = path.join(appleProjectsPath, 'no-files-in-target/Project.xcodeproj/project.pbxproj'); const projectWithSynchronizedFolders = path.join(appleProjectsPath, 'project-with-synchronized-folders/Project.xcodeproj/project.pbxproj'); const singleTargetProjectPath = path.join(appleProjectsPath, 'spm-swiftui-single-target/Project.xcodeproj/project.pbxproj'); const multiTargetProjectPath = path.join(appleProjectsPath, 'spm-swiftui-multi-targets/Project.xcodeproj/project.pbxproj'); const projectData = { id: '1234567890', slug: 'project', organization: { id: '1234567890', name: 'Sentry', slug: 'sentry', }, keys: [{ dsn: { public: 'https://sentry.io/1234567890' } }], }; (0, vitest_1.describe)('XcodeManager', () => { (0, vitest_1.beforeEach)(() => { if (node_process_1.default.platform !== 'darwin') { // The macOS system helpers are only available on macOS // As the test suite is also run on non-macOS platforms, we need to mock the system helpers // The path to the Xcode.app can be different on different machines, so we allow overwriting it using environment variables vitest_1.vi.mock('../../src/apple/macos-system-helper', () => ({ MacOSSystemHelpers: { findSDKRootDirectoryPath: vitest_1.vi.fn(() => '/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk'), findDeveloperDirectoryPath: vitest_1.vi.fn(() => '/Applications/Xcode.app/Contents/Developer'), readXcodeBuildSettings: vitest_1.vi.fn(() => ({ CONFIGURATION_BUILD_DIR: path.join(appleProjectsPath, 'project-with-synchronized-folders/build/Release-unknown'), })), }, })); } }); (0, vitest_1.afterEach)(() => { vitest_1.vi.clearAllMocks(); }); (0, vitest_1.describe)('XcodeProject', () => { (0, vitest_1.describe)('getAllTargets', () => { (0, vitest_1.describe)('single target', () => { (0, vitest_1.it)('should return all targets', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); // -- Act -- const targets = xcodeProject.getAllTargets(); // -- Assert -- (0, vitest_1.expect)(targets).toEqual(['Project']); }); }); (0, vitest_1.describe)('multiple targets', () => { (0, vitest_1.it)('should return all targets', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(multiTargetProjectPath); // -- Act -- const targets = xcodeProject.getAllTargets(); // -- Assert -- (0, vitest_1.expect)(targets).toEqual(['Project1', 'Project2']); }); }); (0, vitest_1.describe)('no targets', () => { (0, vitest_1.it)('should return an empty array', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(noTargetsProjectPath); // -- Act -- const targets = xcodeProject.getAllTargets(); // -- Assert -- (0, vitest_1.expect)(targets).toEqual([]); }); }); (0, vitest_1.describe)('project with missing configuration list', () => { (0, vitest_1.it)('should return an empty array', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(damagedProjectPath); // -- Act -- const targets = xcodeProject.getAllTargets(); // -- Assert -- (0, vitest_1.expect)(targets).toEqual([]); }); }); }); (0, vitest_1.describe)('updateXcodeProject', () => { let sourceProjectPath; let tempProjectPath; let xcodeProject; (0, vitest_1.beforeEach)(() => { const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'update-xcode-project')); sourceProjectPath = singleTargetProjectPath; tempProjectPath = path.resolve(tempDir, 'project.pbxproj'); fs.copyFileSync(sourceProjectPath, tempProjectPath); xcodeProject = new xcode_manager_1.XcodeProject(tempProjectPath); }); (0, vitest_1.describe)('upload symbols script', () => { const scriptVariants = [ { uploadSource: true, includeHomebrewPath: true, }, { uploadSource: true, includeHomebrewPath: false, }, { uploadSource: false, includeHomebrewPath: true, }, { uploadSource: false, includeHomebrewPath: false, }, ]; for (const variant of scriptVariants) { (0, vitest_1.describe)(`upload source = ${variant.uploadSource?.toString()} and include homebrew path = ${variant.includeHomebrewPath.toString()}`, () => { (0, vitest_1.beforeEach)(() => { vitest_1.vi.spyOn(fs, 'existsSync').mockReturnValue(variant.includeHomebrewPath); }); (0, vitest_1.afterEach)(() => { vitest_1.vi.restoreAllMocks(); }); (0, vitest_1.it)('should add the upload symbols script to the target', () => { // -- Arrange -- const generatedShellScript = (0, templates_1.getRunScriptTemplate)(projectData.organization.slug, projectData.slug, variant.uploadSource, variant.includeHomebrewPath); const expectedShellScript = `"${generatedShellScript.replace(/"/g, '\\"')}"`; // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Project', false, // Ignore SPM reference variant.uploadSource); // -- Assert -- const updatedXcodeProject = new xcode_manager_1.XcodeProject(tempProjectPath); // Expect the upload symbols script to be added const scriptObjects = updatedXcodeProject.objects.PBXShellScriptBuildPhase; (0, vitest_1.expect)(scriptObjects).toBeDefined(); if (!scriptObjects) { throw new Error('Script objects not found'); } const scriptKeys = Object.keys(scriptObjects); (0, vitest_1.expect)(scriptKeys).toHaveLength(2); // Find the script ID const scriptId = scriptKeys.find((key) => !key.endsWith('_comment')); (0, vitest_1.expect)(scriptId).toBeDefined(); if (!scriptId) { throw new Error('Script ID not found'); } (0, vitest_1.expect)(scriptId).toMatch(/^[A-F0-9]{24}$/i); // Expect the script to be added const script = scriptObjects[scriptId]; (0, vitest_1.expect)(script).toBeDefined(); (0, vitest_1.expect)(typeof script).not.toBe('string'); (0, vitest_1.expect)(script.inputPaths).toEqual([ '"${DWARF_DSYM_FOLDER_PATH}/${DWARF_DSYM_FILE_NAME}/Contents/Resources/DWARF/${TARGET_NAME}"', ]); (0, vitest_1.expect)(script.outputPaths).toEqual([]); (0, vitest_1.expect)(script.shellPath).toBe('/bin/sh'); (0, vitest_1.expect)(script.shellScript).toEqual(expectedShellScript); const commentKey = `${scriptId}_comment`; (0, vitest_1.expect)(scriptKeys).toContain(commentKey); (0, vitest_1.expect)(scriptObjects[commentKey]).toBe('Upload Debug Symbols to Sentry'); }); }); } }); (0, vitest_1.describe)('debug information format and sandbox', () => { (0, vitest_1.describe)('upload source is false', () => { (0, vitest_1.it)('should not update the Xcode project', () => { // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Project', false, // Ignore SPM reference false); // -- Assert -- const expectedXcodeProject = new xcode_manager_1.XcodeProject(sourceProjectPath); (0, vitest_1.expect)(xcodeProject.objects.XCBuildConfiguration).toEqual(expectedXcodeProject.objects.XCBuildConfiguration); }); }); (0, vitest_1.describe)('upload source is true', () => { const uploadSource = true; (0, vitest_1.describe)('named target not found', () => { (0, vitest_1.it)('should not update the flags in the Xcode project', () => { // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Invalid Target Name', false, // Ignore SPM reference uploadSource); // -- Assert -- const originalXcodeProject = new xcode_manager_1.XcodeProject(sourceProjectPath); (0, vitest_1.expect)(xcodeProject.objects.XCBuildConfiguration).toEqual(originalXcodeProject.objects.XCBuildConfiguration); }); }); (0, vitest_1.describe)('named target found', () => { (0, vitest_1.describe)('build configurations is undefined', () => { (0, vitest_1.it)('should not update the Xcode project', () => { // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Invalid Target Name', false, // Ignore SPM reference uploadSource); // -- Assert -- const originalXcodeProject = new xcode_manager_1.XcodeProject(sourceProjectPath); (0, vitest_1.expect)(xcodeProject.objects.XCBuildConfiguration).toEqual(originalXcodeProject.objects.XCBuildConfiguration); }); }); (0, vitest_1.describe)('no build configurations found', () => { (0, vitest_1.it)('should not update the Xcode project', () => { // -- Arrange -- xcodeProject.objects.XCBuildConfiguration = {}; // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Invalid Target Name', false, // Ignore SPM reference uploadSource); // -- Assert -- (0, vitest_1.expect)(xcodeProject.objects.XCBuildConfiguration).toEqual({}); }); }); (0, vitest_1.describe)('build configurations found', () => { const debugProjectBuildConfigurationListId = 'D4E604DA2D50CEEE00CAB00F'; const releaseProjectBuildConfigurationListId = 'D4E604DB2D50CEEE00CAB00F'; const debugTargetBuildConfigurationListId = 'D4E604DD2D50CEEE00CAB00F'; const releaseTargetBuildConfigurationListId = 'D4E604DE2D50CEEE00CAB00F'; (0, vitest_1.it)('should update the target configuration lists', () => { // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Project', false, // Ignore SPM reference uploadSource); // -- Assert -- (0, vitest_1.expect)(xcodeProject.objects.XCBuildConfiguration).toBeDefined(); // Both Debug and Release are configured equally const expectedConfigKeys = [ debugTargetBuildConfigurationListId, releaseTargetBuildConfigurationListId, // Release ]; for (const key of expectedConfigKeys) { const buildConfiguration = xcodeProject.objects .XCBuildConfiguration?.[key]; (0, vitest_1.expect)(buildConfiguration).toBeDefined(); (0, vitest_1.expect)(typeof buildConfiguration).not.toBe('string'); const buildSettings = buildConfiguration.buildSettings ?? {}; (0, vitest_1.expect)(buildSettings.DEBUG_INFORMATION_FORMAT).toBe('"dwarf-with-dsym"'); (0, vitest_1.expect)(buildSettings.ENABLE_USER_SCRIPT_SANDBOXING).toBe('"NO"'); } }); (0, vitest_1.it)('should not update the project configuration lists', () => { // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Project', false, // Ignore SPM reference uploadSource); // -- Assert -- (0, vitest_1.expect)(xcodeProject.objects.XCBuildConfiguration).toBeDefined(); // Check project build configurations 'Debug' const debugBuildConfiguration = xcodeProject.objects .XCBuildConfiguration?.[debugProjectBuildConfigurationListId]; (0, vitest_1.expect)(debugBuildConfiguration).toBeDefined(); (0, vitest_1.expect)(typeof debugBuildConfiguration).not.toBe('string'); (0, vitest_1.expect)(debugBuildConfiguration.buildSettings ?.DEBUG_INFORMATION_FORMAT).toBe('dwarf'); (0, vitest_1.expect)(debugBuildConfiguration.buildSettings ?.ENABLE_USER_SCRIPT_SANDBOXING).toBe('YES'); // Check project build configurations 'Release' const releaseBuildConfiguration = xcodeProject.objects .XCBuildConfiguration?.[releaseProjectBuildConfigurationListId]; (0, vitest_1.expect)(releaseBuildConfiguration).toBeDefined(); (0, vitest_1.expect)(typeof releaseBuildConfiguration).not.toBe('string'); (0, vitest_1.expect)(releaseBuildConfiguration.buildSettings ?.DEBUG_INFORMATION_FORMAT).toBe('"dwarf-with-dsym"'); (0, vitest_1.expect)(releaseBuildConfiguration.buildSettings ?.ENABLE_USER_SCRIPT_SANDBOXING).toBe('YES'); }); }); }); }); }); (0, vitest_1.describe)('add SPM reference', () => { const addSPMReference = true; (0, vitest_1.describe)('framework build phase already contains Sentry', () => { (0, vitest_1.it)('should not update the Xcode project', () => { // -- Arrange -- xcodeProject.objects.PBXFrameworksBuildPhase = { 'framework-id': { isa: 'PBXFrameworksBuildPhase', files: [ { value: '123', comment: 'Sentry in Frameworks', }, ], }, }; // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Project', addSPMReference); // -- Assert -- const expectedXcodeProject = new xcode_manager_1.XcodeProject(sourceProjectPath); expectedXcodeProject.objects.PBXFrameworksBuildPhase = { 'framework-id': { isa: 'PBXFrameworksBuildPhase', files: [ { value: '123', comment: 'Sentry in Frameworks', }, ], }, }; (0, vitest_1.expect)(xcodeProject.objects.PBXFrameworksBuildPhase).toEqual(expectedXcodeProject.objects.PBXFrameworksBuildPhase); (0, vitest_1.expect)(xcodeProject.objects.XCRemoteSwiftPackageReference).toEqual(expectedXcodeProject.objects.XCRemoteSwiftPackageReference); (0, vitest_1.expect)(xcodeProject.objects.XCSwiftPackageProductDependency).toEqual(expectedXcodeProject.objects.XCSwiftPackageProductDependency); }); }); (0, vitest_1.it)('should add the SPM reference to the target', () => { // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Project', addSPMReference); // -- Assert -- // Get the target const target = xcodeProject.objects.PBXNativeTarget?.['D4E604CC2D50CEEC00CAB00F']; (0, vitest_1.expect)(target).toBeDefined(); if (!target) { throw new Error('Target is undefined'); } // Check the SPM dependency is added to the target (0, vitest_1.expect)(target.packageProductDependencies).toEqual([ vitest_1.expect.objectContaining({ value: vitest_1.expect.any(String), comment: 'Sentry', }), ]); // Check the SPM package reference object is added to the project const remoteSwiftPackageReferences = xcodeProject.objects.XCRemoteSwiftPackageReference; (0, vitest_1.expect)(remoteSwiftPackageReferences).toBeDefined(); if (!remoteSwiftPackageReferences) { throw new Error('XCRemoteSwiftPackageReference is undefined'); } const rspRefKeys = Object.keys(remoteSwiftPackageReferences); (0, vitest_1.expect)(rspRefKeys).toHaveLength(2); // First key is expected to be the UUID of the SPM package reference (0, vitest_1.expect)(rspRefKeys[0]).toMatch(/^[A-F0-9]{24}$/i); // Second key is expected to be the UUID of the SPM package reference with _comment suffix (0, vitest_1.expect)(rspRefKeys[1]).toMatch(/^[A-F0-9]{24}_comment$/i); (0, vitest_1.expect)(remoteSwiftPackageReferences?.[rspRefKeys[0]]).toEqual({ isa: 'XCRemoteSwiftPackageReference', repositoryURL: '"https://github.com/getsentry/sentry-cocoa/"', requirement: { kind: 'upToNextMajorVersion', minimumVersion: '8.0.0', }, }); (0, vitest_1.expect)(remoteSwiftPackageReferences?.[rspRefKeys[1]]).toBe('XCRemoteSwiftPackageReference "sentry-cocoa"'); // Check the SPM package is a dependency of the target const packageProductDependencies = xcodeProject.objects.XCSwiftPackageProductDependency; (0, vitest_1.expect)(packageProductDependencies).toBeDefined(); if (!packageProductDependencies) { throw new Error('XCSwiftPackageProductDependency is undefined'); } const ppDepKeys = Object.keys(packageProductDependencies); (0, vitest_1.expect)(ppDepKeys).toHaveLength(2); // First key is expected to be the UUID of the SPM package dependency (0, vitest_1.expect)(ppDepKeys[0]).toMatch(/^[A-F0-9]{24}$/i); // Second key is expected to be the UUID of the SPM package dependency with _comment suffix (0, vitest_1.expect)(ppDepKeys[1]).toMatch(/^[A-F0-9]{24}_comment$/i); (0, vitest_1.expect)(packageProductDependencies?.[ppDepKeys[0]]).toEqual({ isa: 'XCSwiftPackageProductDependency', package: rspRefKeys[0], package_comment: 'XCRemoteSwiftPackageReference "sentry-cocoa"', productName: 'Sentry', }); }); (0, vitest_1.it)('should initialize packageProductDependencies if not present', () => { // -- Arrange -- // Ensure the target exists but has no packageProductDependencies initially const targetKey = 'D4E604CC2D50CEEC00CAB00F'; const target = xcodeProject.objects.PBXNativeTarget?.[targetKey]; if (target) { // Remove packageProductDependencies to test initialization delete target.packageProductDependencies; } // -- Act -- xcodeProject.updateXcodeProject(projectData, 'Project', addSPMReference); // -- Assert -- const updatedTarget = xcodeProject.objects.PBXNativeTarget?.[targetKey]; (0, vitest_1.expect)(updatedTarget.packageProductDependencies).toBeDefined(); (0, vitest_1.expect)(updatedTarget.packageProductDependencies).toEqual([ vitest_1.expect.objectContaining({ value: vitest_1.expect.any(String), comment: 'Sentry', }), ]); }); }); }); (0, vitest_1.describe)('getSourceFilesForTarget', () => { (0, vitest_1.describe)('targets are undefined', () => { (0, vitest_1.it)('should return undefined', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = undefined; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('Project'); // -- Assert -- (0, vitest_1.expect)(files).toBeUndefined(); }); }); (0, vitest_1.describe)('target not found', () => { (0, vitest_1.it)('should return undefined', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); // -- Act -- const files = xcodeProject.getSourceFilesForTarget('NonExistentTarget'); // -- Assert -- (0, vitest_1.expect)(files).toBeUndefined(); }); }); (0, vitest_1.describe)('target build phases are undefined', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = { Project: { isa: 'PBXNativeTarget', name: 'Project', buildPhases: undefined, }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('Project'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('build phases are undefined', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = { Project: { isa: 'PBXNativeTarget', name: 'Project', buildPhases: undefined, }, }; xcodeProject.objects.PBXSourcesBuildPhase = undefined; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('Project'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('referenced build phase is undefined', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = { Project: { isa: 'PBXNativeTarget', name: 'Project', buildPhases: [ { value: 'random-build-phase', }, ], }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('Project'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('build phase files are undefined', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = { Project: { isa: 'PBXNativeTarget', name: 'Project', buildPhases: [ { value: 'build-phase-key', }, ], }, }; xcodeProject.objects.PBXSourcesBuildPhase = { 'build-phase-key': { isa: 'PBXSourcesBuildPhase', files: undefined, }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('Project'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('build phase has no files', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(noFilesInTargetProjectPath); // -- Act -- const files = xcodeProject.getSourceFilesForTarget('Project'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('build phase with files', () => { let xcodeProject; (0, vitest_1.beforeEach)(() => { xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = { 'some-target': { isa: 'PBXNativeTarget', name: 'some-target', buildPhases: [ { value: 'build-phase-key', }, ], }, }; xcodeProject.objects.PBXSourcesBuildPhase = { 'build-phase-key': { isa: 'PBXSourcesBuildPhase', files: [ { value: 'file-key', }, ], }, }; xcodeProject.objects.PBXBuildFile = { 'file-key': { isa: 'PBXBuildFile', fileRef: 'file-ref-key', }, }; }); (0, vitest_1.describe)('build file objects are not defined', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- xcodeProject.objects.PBXBuildFile = undefined; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('build file object is not found', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- xcodeProject.objects.PBXBuildFile = {}; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('build file object is invalid', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- xcodeProject.objects.PBXBuildFile = { 'file-key': 'invalid-object', }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('file reference is missing', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- xcodeProject.objects.PBXBuildFile = { 'file-key': { isa: 'PBXBuildFile', }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('file reference is invalid', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- xcodeProject.objects.PBXFileReference = { 'file-ref-key': 'invalid-object', }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('file reference path is missing', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- xcodeProject.objects.PBXFileReference = { 'file-ref-key': { isa: 'PBXFileReference', path: undefined, sourceTree: 'SOURCE_ROOT', }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); }); (0, vitest_1.describe)('valid file reference', () => { (0, vitest_1.it)('should return array with file path', () => { // -- Arrange -- xcodeProject.objects.PBXFileReference = { 'file-ref-key': { isa: 'PBXFileReference', path: 'test.swift', sourceTree: 'SOURCE_ROOT', }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([ path.join(xcodeProject.baseDir, 'test.swift'), ]); }); }); }); (0, vitest_1.describe)('synchronized root groups', () => { (0, vitest_1.it)('should handle missing fileSystemSynchronizedGroups', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = { 'some-target': { isa: 'PBXNativeTarget', name: 'some-target', }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); (0, vitest_1.it)('should handle empty fileSystemSynchronizedGroups', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = { 'some-target': { isa: 'PBXNativeTarget', name: 'some-target', fileSystemSynchronizedGroups: [], }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); (0, vitest_1.it)('should handle invalid synchronized root group', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = { 'some-target': { isa: 'PBXNativeTarget', name: 'some-target', fileSystemSynchronizedGroups: [ { value: 'invalid-group', }, ], }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); (0, vitest_1.it)('should handle synchronized root group with missing path', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); xcodeProject.objects.PBXNativeTarget = { 'some-target': { isa: 'PBXNativeTarget', name: 'some-target', fileSystemSynchronizedGroups: [ { value: 'group-key', }, ], }, }; xcodeProject.objects.PBXFileSystemSynchronizedRootGroup = { 'group-key': { isa: 'PBXFileSystemSynchronizedRootGroup', path: undefined, sourceTree: 'SOURCE_ROOT', }, }; // -- Act -- const files = xcodeProject.getSourceFilesForTarget('some-target'); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]); }); (0, vitest_1.it)('should exclude files in membership exceptions', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(projectWithSynchronizedFolders); // The subfolder 1-1-1 is a synchronized root group containing two files: // - File-1-1-1-1.swift // - File-1-1-1-2.swift // // The membership exceptions are: // - File-1-1-1-2.swift // // The expected result is that File-1-1-1-1.swift is excluded from the build, but // included due to the membership exception. // The File-1-1-1-2.swift is excluded from the build due to the membership exception. // Pre-condition: File-1-1-1-1.swift exists const file1111 = path.join(xcodeProject.baseDir, 'Group 1', 'Subgroup 1-1', 'Subfolder 1-1-1', 'File-1-1-1-1.swift'); (0, vitest_1.expect)(fs.existsSync(file1111)).toBe(true); // Pre-condition: File-1-1-1-2.swift exists const file1112 = path.join(xcodeProject.baseDir, 'Group 1', 'Subgroup 1-1', 'Subfolder 1-1-1', 'File-1-1-1-2.swift'); (0, vitest_1.expect)(fs.existsSync(file1112)).toBe(true); // -- Act -- const files = xcodeProject.getSourceFilesForTarget('Project'); // -- Assert -- // Known Issue: // The file `File-1-1-1-1.swift` is included in the source build phase, but not in the list of files. // // This is the group structure: // <main group> / Group 1 / Subgroup 1-1 / Subfolder 1-1-1 / File-1-1-1-1.swift // // - <main group> is the root group // - Group 1 is a group // - Subgroup 1-1 is a nested group // - Subfolder 1-1-1 is a synchronized root group // - File-1-1-1-1.swift is a file in the synchronized root group Subfolder 1-1-1 // // For no apparent reason, Xcode is picking up the file, but Group 1 is not mentioned anywhere other then the main group. // This would require us to consider every root group as a potential source of files, which seems excessive if a project has multiple targets. // expect(files).toContain(file1111); (0, vitest_1.expect)(files).not.toContain(file1112); }); (0, vitest_1.it)('should return synchronized files and files in main group', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(projectWithSynchronizedFolders); // -- Act -- const files = xcodeProject.getSourceFilesForTarget('Project'); // -- Assert -- // The order is not guaranteed, so we need to check for each file individually // The order in this test case is the one displayed in the Xcode UI const group1DirPath = path.join(xcodeProject.baseDir, 'Group 1'); const subgroup1_1DirPath = path.join(group1DirPath, 'Subgroup 1-1'); const subgroup1_2DirPath = path.join(group1DirPath, 'Subgroup 1-2'); const subgroup1_1_2DirPath = path.join(subgroup1_1DirPath, 'Subgroup 1-1-2'); const sourcesDirPath = path.join(xcodeProject.baseDir, 'Sources'); const subfolder1DirPath = path.join(sourcesDirPath, 'Subfolder 1'); const subfolder2DirPath = path.join(sourcesDirPath, 'Subfolder 2'); const groupRef1_3DirPath = path.join(xcodeProject.baseDir, 'Group Reference 1-3'); (0, vitest_1.expect)(files).toContain(path.join(subgroup1_2DirPath, 'File-1-2-2.swift')); (0, vitest_1.expect)(files).toContain(path.join(subfolder1DirPath, 'ContentView.swift')); (0, vitest_1.expect)(files).toContain(path.join(subgroup1_2DirPath, 'File-1-2-3--relative-to-group.swift')); (0, vitest_1.expect)(files).toContain(path.join(sourcesDirPath, 'MainApp.swift')); (0, vitest_1.expect)(files).toContain(path.join(subfolder2DirPath, 'File.swift')); // Absolute path (0, vitest_1.expect)(files).toContain('/System/Library/CoreServices/SystemVersion.plist'); // Path relative to the SDK (0, vitest_1.expect)(files).toContain('/Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk/SDKSettings.plist'); // Path relative to the developer directory (0, vitest_1.expect)(files).toContain('/Applications/Xcode.app/Contents/Developer/usr/bin/git'); // Path relative to the build products directory // NOT SUPPORTED YET // Path relative to the project (0, vitest_1.expect)(files).toContain(path.join(subgroup1_2DirPath, 'File-1-2-1.swift')); (0, vitest_1.expect)(files).toContain(path.join(groupRef1_3DirPath, 'File-1-3-1.swift')); (0, vitest_1.expect)(files).toContain(path.join(subgroup1_2DirPath, 'File-1-2-3--relative-to-project.swift')); (0, vitest_1.expect)(files).toContain(path.join(subgroup1_1_2DirPath, 'File-1-1-2-1.swift')); // Known Issue: // The file `File-1-1-1-1.swift` is included in the source build phase, but not in the list of files. // // This is the group structure: // <main group> / Group 1 / Subgroup 1-1 / Subfolder 1-1-1 / File-1-1-1-1.swift // // - <main group> is the root group // - Group 1 is a group // - Subgroup 1-1 is a nested group // - Subfolder 1-1-1 is a synchronized root group // - File-1-1-1-1.swift is a file in the synchronized root group Subfolder 1-1-1 // // For no apparent reason, Xcode is picking up the file, but Group 1 is not mentioned anywhere other then the main group. // This would require us to consider every root group as a potential source of files, which seems excessive if a project has multiple targets. // expect(files).toContain( // path.join( // xcodeProject.baseDir, // 'Group 1', // 'Subgroup 1-1', // 'Subfolder 1-1-1', // 'File-1-1-1-1.swift', // ), // ); // Assert that there are no other file paths in the list (0, vitest_1.expect)(files).toHaveLength(12); }); }); }); (0, vitest_1.describe)('findFilesInSourceBuildPhase', () => { (0, vitest_1.describe)('when build phase is not found', () => { (0, vitest_1.it)('should return empty array', () => { // -- Arrange -- const xcodeProject = new xcode_manager_1.XcodeProject(singleTargetProjectPath); const nativeTargetId = 'D4E604CC2D50CEEC00CAB00F'; const nativeTarget = xcodeProject.objects.PBXNativeTarget?.[nativeTargetId]; nativeTarget.buildPhases = undefined; // -- Act -- const files = xcodeProject.findFilesInSourceBuildPhase({ id: nativeTargetId, obj: nativeTarget, }); // -- Assert -- (0, vitest_1.expect)(files).toEqual([]);