diff --git a/CHANGELOG.md b/CHANGELOG.md index d5e90a8a82..16c4fe152b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This is the log of notable changes to EAS CLI and related packages. ### 🎉 New features +- Auto-generate `ios.bundleIdentifier` and `android.package` in non-interactive mode when not set in app config. ([#3399](https://github.com/expo/eas-cli/pull/3399) by [@evanbacon](https://github.com/evanbacon)) - Use authorization code flow with PKCE for browser-based login. ([#3398](https://github.com/expo/eas-cli/pull/3398) by [@byronkarlen](https://github.com/byronkarlen)) ### 🐛 Bug fixes diff --git a/packages/eas-cli/src/project/android/__tests__/applicationId-test.ts b/packages/eas-cli/src/project/android/__tests__/applicationId-test.ts index 8517f87a84..82b16d4d21 100644 --- a/packages/eas-cli/src/project/android/__tests__/applicationId-test.ts +++ b/packages/eas-cli/src/project/android/__tests__/applicationId-test.ts @@ -137,21 +137,81 @@ describe(ensureApplicationIdIsDefinedForManagedProjectAsync, () => { }) ).rejects.toThrowError(/we can't update this file programmatically/); }); - it('throws an error in non-interactive mode', async () => { + it('auto-generates application id in non-interactive mode when suggestion is available', async () => { const graphqlClient = instance(mock()); + jest.mocked(AppQuery.byIdAsync).mockResolvedValue({ + id: '1234', + slug: 'testing-123', + name: 'testing-123', + fullName: '@jester/testing-123', + ownerAccount: jester.accounts[0], + }); vol.fromJSON( { - 'app.json': '{ "blah": {} }', + 'app.json': '{ "expo": {} }', }, '/app' ); + + const result = await ensureApplicationIdIsDefinedForManagedProjectAsync({ + graphqlClient, + projectDir: '/app', + projectId: '1234', + exp: { slug: 'myapp' } as any, + vcsClient, + nonInteractive: true, + }); + + expect(result).toBe('com.jester.myapp'); + expect(promptAsync).not.toHaveBeenCalled(); + + const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8')); + expect(appJson).toMatchObject({ + expo: { android: { package: 'com.jester.myapp' } }, + }); + }); + + it('uses ios.bundleIdentifier as application id in non-interactive mode when available', async () => { + const graphqlClient = instance(mock()); + + vol.fromJSON( + { + 'app.json': '{ "expo": {} }', + }, + '/app' + ); + + const result = await ensureApplicationIdIsDefinedForManagedProjectAsync({ + graphqlClient, + projectDir: '/app', + projectId: '1234', + exp: { slug: 'myapp', ios: { bundleIdentifier: 'com.example.myapp' } } as any, + vcsClient, + nonInteractive: true, + }); + + expect(result).toBe('com.example.myapp'); + expect(promptAsync).not.toHaveBeenCalled(); + }); + + it('throws an error in non-interactive mode when no valid suggestion can be generated', async () => { + const graphqlClient = instance(mock()); + jest.mocked(AppQuery.byIdAsync).mockRejectedValue(new Error('Not found')); + + vol.fromJSON( + { + 'app.json': '{ "expo": {} }', + }, + '/app' + ); + await expect( ensureApplicationIdIsDefinedForManagedProjectAsync({ graphqlClient, projectDir: '/app', - projectId: '', - exp: {} as any, + projectId: '1234', + exp: { slug: '' } as any, vcsClient, nonInteractive: true, }) diff --git a/packages/eas-cli/src/project/android/applicationId.ts b/packages/eas-cli/src/project/android/applicationId.ts index 511c4162d4..fb4f3eb93a 100644 --- a/packages/eas-cli/src/project/android/applicationId.ts +++ b/packages/eas-cli/src/project/android/applicationId.ts @@ -148,15 +148,8 @@ async function configureApplicationIdAsync({ exp: ExpoConfig; nonInteractive: boolean; }): Promise { - if (nonInteractive) { - throw new Error( - `The "android.package" is required to be set in app config when running in non-interactive mode. ${learnMore( - 'https://docs.expo.dev/versions/latest/config/app/#package' - )}` - ); - } - const paths = getConfigFilePaths(projectDir); + // we can't automatically update app.config.js if (paths.dynamicConfigPath) { throw new Error( @@ -166,25 +159,50 @@ async function configureApplicationIdAsync({ assert(paths.staticConfigPath, 'app.json must exist'); - Log.addNewLineIfNone(); - Log.log( - `${chalk.bold(`📝 Android application id`)} ${chalk.dim( - learnMore('https://expo.fyi/android-package') - )}` - ); - - const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync( - graphqlClient, - exp, - projectId - ); - const { packageName } = await promptAsync({ - name: 'packageName', - type: 'text', - message: `What would you like your Android application id to be?`, - initial: suggestedAndroidApplicationId, - validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE), - }); + let packageName: string; + + if (nonInteractive) { + let suggestedAndroidApplicationId: string | undefined; + try { + suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync( + graphqlClient, + exp, + projectId + ); + } catch { + // If we can't get a suggestion, we'll throw the non-interactive error below + } + if (!suggestedAndroidApplicationId) { + throw new Error( + `The "android.package" is required to be set in app config when running in non-interactive mode. ${learnMore( + 'https://docs.expo.dev/versions/latest/config/app/#package' + )}` + ); + } + packageName = suggestedAndroidApplicationId; + Log.log(`Using automatically generated Android application id: ${chalk.bold(packageName)}`); + } else { + const suggestedAndroidApplicationId = await getSuggestedApplicationIdAsync( + graphqlClient, + exp, + projectId + ); + Log.addNewLineIfNone(); + Log.log( + `${chalk.bold(`📝 Android application id`)} ${chalk.dim( + learnMore('https://expo.fyi/android-package') + )}` + ); + + const result = await promptAsync({ + name: 'packageName', + type: 'text', + message: `What would you like your Android application id to be?`, + initial: suggestedAndroidApplicationId, + validate: value => (isApplicationIdValid(value) ? true : INVALID_APPLICATION_ID_MESSAGE), + }); + packageName = result.packageName; + } const rawStaticConfig = readAppJson(paths.staticConfigPath); rawStaticConfig.expo = { diff --git a/packages/eas-cli/src/project/ios/__tests__/bundleIdentifier-test.ts b/packages/eas-cli/src/project/ios/__tests__/bundleIdentifier-test.ts index 9be9b731e8..5a59fc0df4 100644 --- a/packages/eas-cli/src/project/ios/__tests__/bundleIdentifier-test.ts +++ b/packages/eas-cli/src/project/ios/__tests__/bundleIdentifier-test.ts @@ -115,20 +115,81 @@ describe(ensureBundleIdentifierIsDefinedForManagedProjectAsync, () => { }) ).rejects.toThrowError(/we can't update this file programmatically/); }); - it('throws an error in non-interactive mode', async () => { + it('auto-generates bundle identifier in non-interactive mode when suggestion is available', async () => { const graphqlClient = instance(mock()); + jest.mocked(AppQuery.byIdAsync).mockResolvedValue({ + id: '1234', + slug: 'testing-123', + name: 'testing-123', + fullName: '@jester/testing-123', + ownerAccount: jester.accounts[0], + }); + vol.fromJSON( { - 'app.json': '{ "blah": {} }', + 'app.json': '{ "expo": {} }', }, '/app' ); + + const result = await ensureBundleIdentifierIsDefinedForManagedProjectAsync({ + graphqlClient, + projectDir: '/app', + projectId: '1234', + exp: { slug: 'myapp' } as any, + vcsClient, + nonInteractive: true, + }); + + expect(result).toBe('com.jester.myapp'); + expect(promptAsync).not.toHaveBeenCalled(); + + const appJson = JSON.parse(await fs.readFile('/app/app.json', 'utf-8')); + expect(appJson).toMatchObject({ + expo: { ios: { bundleIdentifier: 'com.jester.myapp' } }, + }); + }); + + it('uses android.package as bundle identifier in non-interactive mode when available', async () => { + const graphqlClient = instance(mock()); + + vol.fromJSON( + { + 'app.json': '{ "expo": {} }', + }, + '/app' + ); + + const result = await ensureBundleIdentifierIsDefinedForManagedProjectAsync({ + graphqlClient, + projectDir: '/app', + projectId: '1234', + exp: { slug: 'myapp', android: { package: 'com.example.myapp' } } as any, + vcsClient, + nonInteractive: true, + }); + + expect(result).toBe('com.example.myapp'); + expect(promptAsync).not.toHaveBeenCalled(); + }); + + it('throws an error in non-interactive mode when no valid suggestion can be generated', async () => { + const graphqlClient = instance(mock()); + jest.mocked(AppQuery.byIdAsync).mockRejectedValue(new Error('Not found')); + + vol.fromJSON( + { + 'app.json': '{ "expo": {} }', + }, + '/app' + ); + await expect( ensureBundleIdentifierIsDefinedForManagedProjectAsync({ graphqlClient, projectDir: '/app', projectId: '1234', - exp: {} as any, + exp: { slug: '' } as any, vcsClient, nonInteractive: true, }) diff --git a/packages/eas-cli/src/project/ios/bundleIdentifier.ts b/packages/eas-cli/src/project/ios/bundleIdentifier.ts index 07b4108e5c..138afc7bc6 100644 --- a/packages/eas-cli/src/project/ios/bundleIdentifier.ts +++ b/packages/eas-cli/src/project/ios/bundleIdentifier.ts @@ -137,15 +137,8 @@ async function configureBundleIdentifierAsync({ exp: ExpoConfig; nonInteractive: boolean; }): Promise { - if (nonInteractive) { - throw new Error( - `The "ios.bundleIdentifier" is required to be set in app config when running in non-interactive mode. ${learnMore( - 'https://docs.expo.dev/versions/latest/config/app/#bundleidentifier' - )}` - ); - } - const paths = getConfigFilePaths(projectDir); + // we can't automatically update app.config.js if (paths.dynamicConfigPath) { throw new Error( @@ -155,26 +148,51 @@ async function configureBundleIdentifierAsync({ assert(paths.staticConfigPath, 'app.json must exist'); - Log.addNewLineIfNone(); - Log.log( - `${chalk.bold(`📝 iOS Bundle Identifier`)} ${chalk.dim( - learnMore('https://expo.fyi/bundle-identifier') - )}` - ); - - const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync( - graphqlClient, - exp, - projectId - ); - - const { bundleIdentifier } = await promptAsync({ - name: 'bundleIdentifier', - type: 'text', - message: `What would you like your iOS bundle identifier to be?`, - initial: suggestedBundleIdentifier, - validate: value => (isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE), - }); + let bundleIdentifier: string; + + if (nonInteractive) { + let suggestedBundleIdentifier: string | undefined; + try { + suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync( + graphqlClient, + exp, + projectId + ); + } catch { + // If we can't get a suggestion, we'll throw the non-interactive error below + } + if (!suggestedBundleIdentifier) { + throw new Error( + `The "ios.bundleIdentifier" is required to be set in app config when running in non-interactive mode. ${learnMore( + 'https://docs.expo.dev/versions/latest/config/app/#bundleidentifier' + )}` + ); + } + bundleIdentifier = suggestedBundleIdentifier; + Log.log(`Using automatically generated iOS bundle identifier: ${chalk.bold(bundleIdentifier)}`); + } else { + const suggestedBundleIdentifier = await getSuggestedBundleIdentifierAsync( + graphqlClient, + exp, + projectId + ); + Log.addNewLineIfNone(); + Log.log( + `${chalk.bold(`📝 iOS Bundle Identifier`)} ${chalk.dim( + learnMore('https://expo.fyi/bundle-identifier') + )}` + ); + + const result = await promptAsync({ + name: 'bundleIdentifier', + type: 'text', + message: `What would you like your iOS bundle identifier to be?`, + initial: suggestedBundleIdentifier, + validate: value => + isBundleIdentifierValid(value) ? true : INVALID_BUNDLE_IDENTIFIER_MESSAGE, + }); + bundleIdentifier = result.bundleIdentifier; + } const rawStaticConfig = readAppJson(paths.staticConfigPath); rawStaticConfig.expo = {