diff --git a/CHANGELOG.md b/CHANGELOG.md index 814d752e76..2a98a29bf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/packages/eas-cli/graphql.schema.json b/packages/eas-cli/graphql.schema.json index 466852b2bb..33edae918b 100644 --- a/packages/eas-cli/graphql.schema.json +++ b/packages/eas-cli/graphql.schema.json @@ -9764,6 +9764,22 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "suggestedDevDomainName", + "description": null, + "args": [], + "type": { + "kind": "NON_NULL", + "name": null, + "ofType": { + "kind": "SCALAR", + "name": "String", + "ofType": null + } + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "timelineActivity", "description": "Coalesced project activity for an app using pagination", @@ -51202,6 +51218,18 @@ "isDeprecated": false, "deprecationReason": null }, + { + "name": "initiatingActor", + "description": null, + "args": [], + "type": { + "kind": "INTERFACE", + "name": "Actor", + "ofType": null + }, + "isDeprecated": false, + "deprecationReason": null + }, { "name": "logs", "description": null, diff --git a/packages/eas-cli/src/commands/worker/deploy.ts b/packages/eas-cli/src/commands/worker/deploy.ts index 83bf1938ef..79e8044e5c 100644 --- a/packages/eas-cli/src/commands/worker/deploy.ts +++ b/packages/eas-cli/src/commands/worker/deploy.ts @@ -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'); diff --git a/packages/eas-cli/src/graphql/generated.ts b/packages/eas-cli/src/graphql/generated.ts index 50f4a66c0d..eb7cf6d8e8 100644 --- a/packages/eas-cli/src/graphql/generated.ts +++ b/packages/eas-cli/src/graphql/generated.ts @@ -1333,6 +1333,7 @@ export type App = Project & { /** EAS Submissions associated with this app */ submissions: Array; submissionsPaginated: AppSubmissionsConnection; + suggestedDevDomainName: Scalars['String']['output']; /** Coalesced project activity for an app using pagination */ timelineActivity: TimelineActivityConnection; /** @deprecated 'likes' have been deprecated. */ @@ -7394,6 +7395,7 @@ export type WorkerDeployment = { deploymentIdentifier: Scalars['WorkerDeploymentIdentifier']['output']; devDomainName: Scalars['DevDomainName']['output']; id: Scalars['ID']['output']; + initiatingActor?: Maybe; logs?: Maybe; requests?: Maybe; subdomain: Scalars['String']['output']; @@ -8755,3 +8757,10 @@ export type PaginatedWorkerDeploymentsQueryVariables = Exact<{ export type PaginatedWorkerDeploymentsQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, workerDeployments: { __typename?: 'WorkerDeploymentsConnection', pageInfo: { __typename?: 'PageInfo', hasNextPage: boolean, hasPreviousPage: boolean, startCursor?: string | null, endCursor?: string | null }, edges: Array<{ __typename?: 'WorkerDeploymentEdge', cursor: string, node: { __typename?: 'WorkerDeployment', id: string, url: string, deploymentIdentifier: any, deploymentDomain: string, createdAt: any } }> } } } }; + +export type SuggestedDevDomainNameQueryVariables = Exact<{ + appId: Scalars['String']['input']; +}>; + + +export type SuggestedDevDomainNameQuery = { __typename?: 'RootQuery', app: { __typename?: 'AppQuery', byId: { __typename?: 'App', id: string, suggestedDevDomainName: string } } }; diff --git a/packages/eas-cli/src/worker/deployment.ts b/packages/eas-cli/src/worker/deployment.ts index 0ad8c8b553..34408c3b9c 100644 --- a/packages/eas-cli/src/worker/deployment.ts +++ b/packages/eas-cli/src/worker/deployment.ts @@ -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 { try { @@ -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( @@ -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 { - 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 { @@ -74,7 +138,7 @@ 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 => @@ -82,8 +146,8 @@ async function chooseDevDomainNameAsync({ ); 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) { diff --git a/packages/eas-cli/src/worker/queries.ts b/packages/eas-cli/src/worker/queries.ts index 6622bebaa4..33404d2e22 100644 --- a/packages/eas-cli/src/worker/queries.ts +++ b/packages/eas-cli/src/worker/queries.ts @@ -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'; @@ -58,4 +60,30 @@ export const DeploymentsQuery = { return data.app.byId.workerDeployments; }, + + async getSuggestedDevDomainByAppIdAsync( + graphqlClient: ExpoGraphqlClient, + { appId }: SuggestedDevDomainNameQueryVariables + ): Promise { + const data = await withErrorHandlingAsync( + graphqlClient + .query( + gql` + query SuggestedDevDomainName($appId: String!) { + app { + byId(appId: $appId) { + id + suggestedDevDomainName + } + } + } + `, + { appId }, + { additionalTypenames: ['App'] } + ) + .toPromise() + ); + + return data.app.byId.suggestedDevDomainName; + }, }; diff --git a/packages/eas-cli/src/worker/utils/logs.ts b/packages/eas-cli/src/worker/utils/logs.ts index 4de0f6c277..67e3ced846 100644 --- a/packages/eas-cli/src/worker/utils/logs.ts +++ b/packages/eas-cli/src/worker/utils/logs.ts @@ -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`;