Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExpoGraphqlClient>());
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<ExpoGraphqlClient>());

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<ExpoGraphqlClient>());
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,
})
Expand Down
72 changes: 45 additions & 27 deletions packages/eas-cli/src/project/android/applicationId.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,15 +148,8 @@ async function configureApplicationIdAsync({
exp: ExpoConfig;
nonInteractive: boolean;
}): Promise<string> {
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(
Expand All @@ -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 = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExpoGraphqlClient>());
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<ExpoGraphqlClient>());

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<ExpoGraphqlClient>());
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,
})
Expand Down
74 changes: 46 additions & 28 deletions packages/eas-cli/src/project/ios/bundleIdentifier.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,8 @@ async function configureBundleIdentifierAsync({
exp: ExpoConfig;
nonInteractive: boolean;
}): Promise<string> {
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(
Expand All @@ -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 = {
Expand Down