Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(eas-cli): polish the dev domain prompt #2564

Merged
merged 4 commits into from
Sep 14, 2024
Merged
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 @@ -29,6 +29,7 @@ This is the log of notable changes to EAS CLI and related packages.
- Support `worker --production` and clean up command output. ([#2555](https://github.com/expo/eas-cli/pull/2555) by [@byCedric](https://github.com/byCedric)))
- Unify both `worker` and `worker:alias` command output. ([#2558](https://github.com/expo/eas-cli/pull/2558) by [@byCedric](https://github.com/byCedric)))
- Share similar table/json output in both `worker` and `worker:alias` command outputs. ([#2563](https://github.com/expo/eas-cli/pull/2563) by [@byCedric](https://github.com/byCedric)))
- Polish the project URL prompt when setting up new projects. ([#2564](https://github.com/expo/eas-cli/pull/2564) by [@byCedric](https://github.com/byCedric)))

## [12.3.0](https://github.com/expo/eas-cli/releases/tag/v12.3.0) - 2024-09-09

Expand Down
28 changes: 28 additions & 0 deletions packages/eas-cli/graphql.schema.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 4 additions & 4 deletions packages/eas-cli/src/commands/worker/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -260,12 +260,12 @@ export default class WorkerDeploy extends EasCommand {
})
);

// NOTE(cedric): this function might ask the user for a dev-domain name,
// when that happens, no ora spinner should be running.
progress.stop();
const uploadUrl = await getSignedDeploymentUrlAsync(graphqlClient, exp, {
const uploadUrl = await getSignedDeploymentUrlAsync(graphqlClient, {
appId: projectId,
deploymentIdentifier: flags.deploymentIdentifier,
// NOTE(cedric): this function might ask the user for a dev-domain name,
// when that happens, no ora spinner should be running.
onSetupDevDomain: () => progress.stop(),
});

progress.start('Creating deployment');
Expand Down
9 changes: 9 additions & 0 deletions packages/eas-cli/src/graphql/generated.ts

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

90 changes: 77 additions & 13 deletions packages/eas-cli/src/worker/deployment.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,23 @@
import { ExpoConfig } from '@expo/config-types';
import { CombinedError as GraphqlError } from '@urql/core';
import chalk from 'chalk';

import { DeploymentsMutation } from './mutations';
import { DeploymentsQuery } from './queries';
import { EXPO_BASE_DOMAIN } from './utils/logs';
import { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { WorkerDeploymentFragment } from '../graphql/generated';
import Log from '../log';
import { promptAsync } from '../prompts';
import { memoize } from '../utils/expodash/memoize';
import { selectPaginatedAsync } from '../utils/relay';

export async function getSignedDeploymentUrlAsync(
graphqlClient: ExpoGraphqlClient,
exp: ExpoConfig,
deploymentVariables: {
appId: string;
deploymentIdentifier?: string | null;
/** Callback which is invoked when the project is going to setup the dev domain */
onSetupDevDomain?: () => any;
}
): Promise<string> {
try {
Expand All @@ -32,10 +34,17 @@ export async function getSignedDeploymentUrlAsync(
throw error;
}

const suggestedDevDomainName = await DeploymentsQuery.getSuggestedDevDomainByAppIdAsync(
graphqlClient,
{ appId: deploymentVariables.appId }
);

deploymentVariables.onSetupDevDomain?.();

await chooseDevDomainNameAsync({
graphqlClient,
appId: deploymentVariables.appId,
slug: exp.slug,
initial: suggestedDevDomainName,
});

return await DeploymentsMutation.createSignedDeploymentUrlAsync(
Expand All @@ -45,26 +54,81 @@ export async function getSignedDeploymentUrlAsync(
}
}

type PromptInstance = {
cursorOffset: number;
placeholder: boolean;
rendered: string;
initial: string;
get value(): string;
set value(input: string);
};

const DEV_DOMAIN_INVALID_START_END_CHARACTERS = /^[^a-z0-9]+|[^a-z0-9-]+$/;
const DEV_DOMAIN_INVALID_REPLACEMENT_HYPHEN = /[^a-z0-9-]+/;
const DEV_DOMAIN_INVALID_MULTIPLE_HYPHENS = /(-{2,})/;

/**
* Format a dev domain name to match whats allowed on the backend.
* This is equal to our `DEV_DOMAIN_NAME_REGEX`, but implemented as a filtering function
* to help users find a valid name while typing.
*/
function formatDevDomainName(name = ''): string {
return name
.toLowerCase()
.replace(DEV_DOMAIN_INVALID_REPLACEMENT_HYPHEN, '-')
.replace(DEV_DOMAIN_INVALID_START_END_CHARACTERS, '')
.replace(DEV_DOMAIN_INVALID_MULTIPLE_HYPHENS, '-')
.trim();
}

async function chooseDevDomainNameAsync({
graphqlClient,
appId,
slug,
initial,
}: {
graphqlClient: ExpoGraphqlClient;
appId: string;
slug: string;
initial: string;
}): Promise<void> {
const validationMessage = 'The project does not have a dev domain name.';
const rootDomain = `.${EXPO_BASE_DOMAIN}.app`;
const memoizedFormatDevDomainName = memoize(formatDevDomainName);

const { name } = await promptAsync({
type: 'text',
name: 'name',
message: 'Choose a dev domain name for your project:',
validate: value => (value && value.length > 3 ? true : validationMessage),
initial: slug,
message: 'Choose a URL for your project:',
initial,
validate: (value: string) => {
if (!value) {
return 'You have to choose a URL for your project';
}
if (value.length < 3) {
return 'Project URLs must be at least 3 characters long';
}
if (value.endsWith('-')) {
return 'Project URLs cannot end with a hyphen (-)';
}
return true;
},
onState(this: PromptInstance, state: { value?: string }) {
const value = memoizedFormatDevDomainName(state.value);
if (value !== state.value) {
this.value = value;
}
},
onRender(this: PromptInstance, kleur) {
this.cursorOffset = -rootDomain.length - 1;

if (this.placeholder) {
this.rendered = kleur.dim(`${this.initial} ${rootDomain}`);
} else {
this.rendered = this.value + kleur.dim(` ${rootDomain}`);
}
},
});

if (!name) {
throw new Error('Prompt failed');
throw new Error('No project URL provided, aborting deployment.');
}

try {
Expand All @@ -74,16 +138,16 @@ async function chooseDevDomainNameAsync({
});

if (!success) {
throw new Error('Failed to assign dev domain name');
throw new Error('Failed to assign project URL');
}
} catch (error: any) {
const isChosenNameTaken = (error as GraphqlError)?.graphQLErrors?.some(e =>
['DEV_DOMAIN_NAME_TAKEN'].includes(e?.extensions?.errorCode as string)
);

if (isChosenNameTaken) {
Log.error(`The entered dev domain name "${name}" is taken. Choose a different name.`);
await chooseDevDomainNameAsync({ graphqlClient, appId, slug });
Log.error(`The project URL "${name}" is already taken, choose a different name.`);
await chooseDevDomainNameAsync({ graphqlClient, appId, initial });
}

if (!isChosenNameTaken) {
Expand Down
36 changes: 32 additions & 4 deletions packages/eas-cli/src/worker/queries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import gql from 'graphql-tag';
import { WorkerDeploymentFragmentNode } from './fragments/WorkerDeployment';
import type { ExpoGraphqlClient } from '../commandUtils/context/contextUtils/createGraphqlClient';
import { withErrorHandlingAsync } from '../graphql/client';
import type {
PaginatedWorkerDeploymentsQuery,
PaginatedWorkerDeploymentsQueryVariables,
WorkerDeploymentFragment,
import {
type PaginatedWorkerDeploymentsQuery,
type PaginatedWorkerDeploymentsQueryVariables,
SuggestedDevDomainNameQuery,
SuggestedDevDomainNameQueryVariables,
type WorkerDeploymentFragment,
} from '../graphql/generated';
import type { Connection } from '../utils/relay';

Expand Down Expand Up @@ -58,4 +60,30 @@ export const DeploymentsQuery = {

return data.app.byId.workerDeployments;
},

async getSuggestedDevDomainByAppIdAsync(
graphqlClient: ExpoGraphqlClient,
{ appId }: SuggestedDevDomainNameQueryVariables
): Promise<string> {
const data = await withErrorHandlingAsync(
graphqlClient
.query<SuggestedDevDomainNameQuery, SuggestedDevDomainNameQueryVariables>(
gql`
query SuggestedDevDomainName($appId: String!) {
app {
byId(appId: $appId) {
id
suggestedDevDomainName
}
}
}
`,
{ appId },
{ additionalTypenames: ['App'] }
)
.toPromise()
);

return data.app.byId.suggestedDevDomainName;
},
};
2 changes: 1 addition & 1 deletion packages/eas-cli/src/worker/utils/logs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
} from '../../graphql/generated';
import formatFields, { type FormatFieldsItem } from '../../utils/formatFields';

const EXPO_BASE_DOMAIN = process.env.EXPO_STAGING ? 'staging.expo' : 'expo';
export const EXPO_BASE_DOMAIN = process.env.EXPO_STAGING ? 'staging.expo' : 'expo';

export function getDeploymentUrlFromFullName(deploymentFullName: string): string {
return `https://${deploymentFullName}.${EXPO_BASE_DOMAIN}.app`;
Expand Down
Loading