diff --git a/packages/cli/src/platforms/ios/entitlements.ts b/packages/cli/src/platforms/ios/entitlements.ts index b465ddd9..48232859 100644 --- a/packages/cli/src/platforms/ios/entitlements.ts +++ b/packages/cli/src/platforms/ios/entitlements.ts @@ -1,8 +1,9 @@ -import { readTextFile, writeTextFile } from '../../fs/readWrite' +import { pathExists, readTextFile, writeTextFile } from '../../fs/readWrite' import { toRelativePath } from '../../fs/path' import { VoltraCliError } from '../../reporting/summary' import { buildPlistXml, parsePlistFile } from './plist' +import { resolveMainAppEntitlementsPath } from './mainAppEntitlements' import type { IOSProjectDiscovery } from '../../discovery/ios' import type { NormalizedVoltraIOSConfig } from '../../config/types' @@ -33,22 +34,17 @@ export class IOSEntitlementsMutationError extends VoltraCliError { export async function ensureEntitlements(options: EnsureEntitlementsOptions): Promise { const { projectRoot, ios, discovery } = options + const entitlementsPath = resolveMainAppEntitlementsPath(discovery) if (!discovery.entitlementsPath) { if (!needsEntitlementsMutation(ios)) { return {} } - - throw new IOSEntitlementsMutationError( - `Could not determine the main app entitlements file. Set ios.project.entitlementsPath to update entitlements for target '${discovery.mainTargetName}'.` - ) } - const entitlements = await parsePlistFile( - discovery.entitlementsPath, - 'main app entitlements', - createEntitlementsError - ) + const entitlements: Record = (await pathExists(entitlementsPath)) + ? await parsePlistFile(entitlementsPath, 'main app entitlements', createEntitlementsError) + : {} const previousVoltraValues = await readPreviousVoltraEntitlementValues(discovery.infoPlistPath) ensureStringArrayValue( @@ -70,12 +66,12 @@ export async function ensureEntitlements(options: EnsureEntitlementsOptions): Pr } const nextContent = buildPlistXml(entitlements, createEntitlementsError) - const change = await writeEntitlementsIfChanged(projectRoot, discovery.entitlementsPath, nextContent) + const change = await writeEntitlementsIfChanged(projectRoot, entitlementsPath, nextContent) return { change } } -function needsEntitlementsMutation(ios: NormalizedVoltraIOSConfig): boolean { +export function needsEntitlementsMutation(ios: NormalizedVoltraIOSConfig): boolean { return ios.enablePushNotifications || ios.groupIdentifier !== undefined || ios.keychainGroup !== undefined } @@ -120,7 +116,7 @@ async function writeEntitlementsIfChanged( entitlementsPath: string, nextContent: string ): Promise { - const previousContent = await readTextFile(entitlementsPath) + const previousContent = (await pathExists(entitlementsPath)) ? await readTextFile(entitlementsPath) : undefined if (previousContent === nextContent) { return undefined @@ -129,7 +125,7 @@ async function writeEntitlementsIfChanged( await writeTextFile(entitlementsPath, nextContent) return { - kind: 'updated', + kind: previousContent === undefined ? 'created' : 'updated', path: toRelativePath(projectRoot, entitlementsPath), } } diff --git a/packages/cli/src/platforms/ios/mainAppEntitlements.ts b/packages/cli/src/platforms/ios/mainAppEntitlements.ts new file mode 100644 index 00000000..1bf83436 --- /dev/null +++ b/packages/cli/src/platforms/ios/mainAppEntitlements.ts @@ -0,0 +1,11 @@ +import path from 'node:path' + +import type { IOSProjectDiscovery } from '../../discovery/ios' + +export function resolveMainAppEntitlementsPath(discovery: IOSProjectDiscovery): string { + if (discovery.entitlementsPath) { + return discovery.entitlementsPath + } + + return path.join(path.dirname(discovery.infoPlistPath), `${discovery.mainTargetName}.entitlements`) +} diff --git a/packages/cli/src/platforms/ios/xcodeTarget.ts b/packages/cli/src/platforms/ios/xcodeTarget.ts index 1f22c651..173fc950 100644 --- a/packages/cli/src/platforms/ios/xcodeTarget.ts +++ b/packages/cli/src/platforms/ios/xcodeTarget.ts @@ -12,9 +12,12 @@ import { } from '@bacons/xcode' import { normalizeRelativePath, toRelativePath } from '../../fs/path' +import { pathExists } from '../../fs/readWrite' import { VoltraCliError } from '../../reporting/summary' import { resolveIOSWidgetTargetName } from './targetName' import { ensureMainGroupChild, openIOSXcodeProject, saveIOSXcodeProject } from './xcode' +import { resolveMainAppEntitlementsPath } from './mainAppEntitlements' +import { needsEntitlementsMutation } from './entitlements' import type { IOSProjectDiscovery } from '../../discovery/ios' import type { NormalizedVoltraIOSConfig } from '../../config/types' @@ -65,7 +68,7 @@ export async function ensureIOSWidgetTarget( const staleTargetNames = getStaleWidgetTargetNames(previousWidgetFiles, targetName) const bundleIdentifier = resolveBundleIdentifier(context, discovery, targetName) const codeSigning = getMainAppCodeSigningSettings(context) - const mainAppEntitlementsPath = getMainAppEntitlementsBuildSetting(projectRoot, discovery) + const mainAppEntitlementsPath = await getMainAppEntitlementsBuildSetting(discovery, ios) removeStaleWidgetTargets(context, staleTargetNames) ensureMainAppEntitlementsBuildSetting(context, mainAppEntitlementsPath) @@ -739,12 +742,25 @@ function getMainAppCodeSigningSettings(context: IOSXcodeProjectContext): MainApp } } -function getMainAppEntitlementsBuildSetting(projectRoot: string, discovery: IOSProjectDiscovery): string | undefined { - if (!discovery.entitlementsPath) { +async function getMainAppEntitlementsBuildSetting( + discovery: IOSProjectDiscovery, + ios: NormalizedVoltraIOSConfig +): Promise { + if (discovery.entitlementsPath) { + return normalizeRelativePath(path.relative(discovery.iosRoot, discovery.entitlementsPath)) + } + + if (!needsEntitlementsMutation(ios)) { + return undefined + } + + const entitlementsPath = resolveMainAppEntitlementsPath(discovery) + + if (!(await pathExists(entitlementsPath))) { return undefined } - return normalizeRelativePath(path.relative(discovery.iosRoot, discovery.entitlementsPath)) + return normalizeRelativePath(path.relative(discovery.iosRoot, entitlementsPath)) } function ensureMainAppEntitlementsBuildSetting( diff --git a/packages/cli/test/cli.test.js b/packages/cli/test/cli.test.js index e2a1e50e..9fcfa0e2 100644 --- a/packages/cli/test/cli.test.js +++ b/packages/cli/test/cli.test.js @@ -11,6 +11,10 @@ function loadCliModule() { return require(path.join(packageRoot, 'build/cjs/index.js')) } +function loadIosMainAppEntitlementsModule() { + return require(path.join(packageRoot, 'build/cjs/platforms/ios/mainAppEntitlements.js')) +} + function writeFakePackage(projectRoot, packageName) { const packagePath = path.join(projectRoot, 'node_modules', ...packageName.split('/'), 'package.json') fs.mkdirSync(path.dirname(packagePath), { recursive: true }) @@ -130,6 +134,68 @@ test('ios preflight reports missing client package when renderer is installed', assert.doesNotMatch(result.issues[0].message, /@use-voltra\/ios and/) }) +test('resolves the standard main app entitlements path when discovery is missing one', () => { + const { resolveMainAppEntitlementsPath } = loadIosMainAppEntitlementsModule() + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'voltra-cli-test-')) + + const discovery = { + iosRoot: path.join(tempDir, 'ios'), + xcodeprojPath: path.join(tempDir, 'ios', 'TestApp.xcodeproj'), + pbxprojPath: path.join(tempDir, 'ios', 'TestApp.xcodeproj', 'project.pbxproj'), + podfilePath: path.join(tempDir, 'ios', 'Podfile'), + mainTargetName: 'TestApp', + mainTargetCandidates: ['TestApp'], + infoPlistPath: path.join(tempDir, 'ios', 'TestApp', 'Info.plist'), + } + + assert.equal(resolveMainAppEntitlementsPath(discovery), path.join(tempDir, 'ios', 'TestApp', 'TestApp.entitlements')) +}) + +test('ensureEntitlements creates the main app entitlements file when it is missing', async () => { + const { ensureEntitlements } = loadCliModule() + const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'voltra-cli-test-')) + const iosRoot = path.join(tempDir, 'ios') + const infoPlistPath = path.join(iosRoot, 'TestApp', 'Info.plist') + const entitlementsPath = path.join(iosRoot, 'TestApp', 'TestApp.entitlements') + + fs.mkdirSync(path.dirname(infoPlistPath), { recursive: true }) + fs.writeFileSync( + infoPlistPath, + ` + + + + +` + ) + + const result = await ensureEntitlements({ + projectRoot: tempDir, + ios: { + enablePushNotifications: true, + groupIdentifier: 'group.com.example.app', + project: {}, + }, + discovery: { + iosRoot, + xcodeprojPath: path.join(iosRoot, 'TestApp.xcodeproj'), + pbxprojPath: path.join(iosRoot, 'TestApp.xcodeproj', 'project.pbxproj'), + podfilePath: path.join(iosRoot, 'Podfile'), + mainTargetName: 'TestApp', + mainTargetCandidates: ['TestApp'], + infoPlistPath, + }, + }) + + assert.ok(result.change) + assert.equal(result.change.kind, 'created') + assert.equal(fs.existsSync(entitlementsPath), true) + const entitlements = fs.readFileSync(entitlementsPath, 'utf8') + assert.match(entitlements, /com\.apple\.security\.application-groups/) + assert.match(entitlements, /group\.com\.example\.app/) + assert.match(entitlements, /aps-environment/) +}) + test('android preflight reports missing client package when renderer is installed', async () => { const { createAndroidPreflightRunner } = loadCliModule() const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'voltra-cli-test-'))