From 5be54c6f75a168cf8be43842c774b21f35499a43 Mon Sep 17 00:00:00 2001 From: Evan Bacon Date: Sun, 27 Dec 2020 13:09:51 -0800 Subject: [PATCH] [cli] Mark expo upload:ios as unsupported (#3030) --- packages/expo-cli/src/commands/upload.ts | 55 ++--- .../src/commands/upload/BaseUploader.ts | 140 ------------ .../src/commands/upload/IOSUploader.ts | 204 ------------------ .../expo-cli/src/commands/upload/utils.ts | 66 ------ .../src/commands/utils/TerminalLink.ts | 8 + 5 files changed, 26 insertions(+), 447 deletions(-) delete mode 100644 packages/expo-cli/src/commands/upload/BaseUploader.ts delete mode 100644 packages/expo-cli/src/commands/upload/IOSUploader.ts delete mode 100644 packages/expo-cli/src/commands/upload/utils.ts diff --git a/packages/expo-cli/src/commands/upload.ts b/packages/expo-cli/src/commands/upload.ts index 89c91e95b7..2114576981 100644 --- a/packages/expo-cli/src/commands/upload.ts +++ b/packages/expo-cli/src/commands/upload.ts @@ -1,16 +1,11 @@ import chalk from 'chalk'; import { Command } from 'commander'; -import pick from 'lodash/pick'; -import CommandError from '../CommandError'; import log from '../log'; -import IOSUploader, { IosPlatformOptions, LANGUAGES } from './upload/IOSUploader'; import AndroidSubmitCommand from './upload/submission-service/android/AndroidSubmitCommand'; import { AndroidSubmitCommandOptions } from './upload/submission-service/android/types'; import * as TerminalLink from './utils/TerminalLink'; -const SOURCE_OPTIONS = ['id', 'latest', 'path', 'url']; - export default function (program: Command) { program .command('upload:android [path]') @@ -59,7 +54,9 @@ export default function (program: Command) { program .command('upload:ios [path]') .alias('ui') - .description('macOS only: Upload an iOS binary to Apple. An alternative to Transporter.app') + .description( + `${chalk.yellow('Unsupported:')} Use ${chalk.bold('eas submit')} or Transporter app instead.` + ) .longDescription( 'Upload an iOS binary to Apple TestFlight (MacOS only). Uses the latest build by default' ) @@ -101,38 +98,22 @@ export default function (program: Command) { 'English' ) .option('--public-url ', 'The URL of an externally hosted manifest (for self-hosted apps)') - - .on('--help', function () { - log('Available languages:'); - log(` ${LANGUAGES.join(', ')}`); - log(); - }) // TODO: make this work outside the project directory (if someone passes all necessary options for upload) - .asyncActionProjectDir(async (projectDir: string, options: IosPlatformOptions) => { - try { - // TODO: remove this once we remove fastlane - if (process.platform !== 'darwin') { - throw new CommandError('Currently, iOS uploads are only supported on macOS, sorry :('); - } + .asyncActionProjectDir(async (projectDir: string, options: any) => { + const logItem = (name: string, link: string) => { + log(`\u203A ${TerminalLink.linkedText(name, link)}`); + }; - const args = pick(options, SOURCE_OPTIONS); - if (Object.keys(args).length > 1) { - throw new Error(`You have to choose only one of: --path, --id, --latest, --url`); - } - IOSUploader.validateOptions(options); - const uploader = new IOSUploader(projectDir, options); - await uploader.upload(); - } catch (err) { - log.error('Failed to upload the standalone app to the App Store.'); - log.warn( - `We recommend using ${chalk.bold( - TerminalLink.transporterAppLink() - )} instead of the ${chalk.bold( - 'expo upload:ios' - )} command if you have any trouble with it.` - ); - - throw err; - } + log.newLine(); + log(chalk.yellow('expo upload:ios is no longer supported')); + log('Please use one of the following'); + log.newLine(); + logItem(chalk.cyan.bold('eas submit'), 'https://docs.expo.io/submit/ios'); + logItem('Transporter', 'https://apps.apple.com/us/app/transporter/id1450874784'); + logItem( + 'Fastlane deliver', + 'https://docs.fastlane.tools/getting-started/ios/appstore-deployment' + ); + log.newLine(); }); } diff --git a/packages/expo-cli/src/commands/upload/BaseUploader.ts b/packages/expo-cli/src/commands/upload/BaseUploader.ts deleted file mode 100644 index 843343d149..0000000000 --- a/packages/expo-cli/src/commands/upload/BaseUploader.ts +++ /dev/null @@ -1,140 +0,0 @@ -import { ExpoConfig, getConfig, Platform } from '@expo/config'; -import { StandaloneBuild } from '@expo/xdl'; -import chalk from 'chalk'; -import fs from 'fs-extra'; -import path from 'path'; - -import log from '../../log'; -import { - downloadAppArchiveAsync, - extractLocalArchiveAsync, -} from './submission-service/utils/files'; - -export type PlatformOptions = { - id?: string; - path?: string; - url?: string; -}; - -export default class BaseUploader { - _exp?: ExpoConfig; - fastlane: { [key: string]: string }; - - constructor( - public platform: Platform, - public projectDir: string, - public options: PlatformOptions - ) { - // it has to happen in constructor because we don't want to load this module on a different platform than darwin - this.fastlane = require('@expo/traveling-fastlane-darwin')(); - } - - async upload(): Promise { - await this._getProjectConfig(); - const platformData = await this._getPlatformSpecificOptions(); - const buildPath = await this._getBinaryFilePath(); - await this._uploadToTheStore(platformData, buildPath); - await this._removeBuildFileIfDownloaded(buildPath); - log( - `Please also see our docs (${chalk.underline( - 'https://docs.expo.io/distribution/uploading-apps/' - )}) to learn more about the upload process.` - ); - } - - async _getProjectConfig(): Promise { - const { exp } = getConfig(this.projectDir, { - skipSDKVersionRequirement: true, - }); - this._ensureExperienceIsValid(exp); - this._exp = exp; - } - - async _getBinaryFilePath(): Promise { - const { path, id, url } = this.options; - if (path) { - return this._downloadBuild(path); - } else if (id) { - return this._downloadBuildById(id); - } else if (url) { - return this._downloadBuild(url); - } else { - return this._downloadLastestBuild(); - } - } - - async _downloadBuildById(id: string): Promise { - const { platform } = this; - const slug = this._getSlug(); - const owner = this._getOwner(); - const build = await StandaloneBuild.getStandaloneBuildById({ id, slug, platform, owner }); - if (!build) { - throw new Error(`We couldn't find build with id ${id}`); - } - return this._downloadBuild(build.artifacts.url); - } - - _getSlug(): string { - if (!this._exp || !this._exp.slug) { - throw new Error(`slug doesn't exist`); - } - return this._exp.slug; - } - - _getOwner(): string | undefined { - if (!this._exp || !this._exp.owner) { - return undefined; - } - return this._exp.owner; - } - - async _downloadLastestBuild() { - const { platform } = this; - - const slug = this._getSlug(); - const owner = this._getOwner(); - const builds = await StandaloneBuild.getStandaloneBuilds( - { - slug, - owner, - platform, - }, - 1 - ); - if (builds.length === 0) { - throw new Error( - `There are no builds on the Expo servers, please run 'expo build:${platform}' first` - ); - } - return this._downloadBuild(builds[0].artifacts.url); - } - - async _downloadBuild(urlOrPath: string): Promise { - if (path.isAbsolute(urlOrPath)) { - // Local file paths that don't need to be extracted will simply return the `urlOrPath` as the final destination. - return await extractLocalArchiveAsync(urlOrPath); - } else { - // Remote files - log(`Downloading build from ${urlOrPath}`); - return await downloadAppArchiveAsync(urlOrPath); - } - } - - async _removeBuildFileIfDownloaded(buildPath: string): Promise { - if (!this.options.path) { - await fs.remove(buildPath); - } - } - - _ensureExperienceIsValid(exp: ExpoConfig): void { - throw new Error('Not implemented'); - } - - async _getPlatformSpecificOptions(): Promise<{ [key: string]: any }> { - throw new Error('Not implemented'); - } - - async _uploadToTheStore(platformData: PlatformOptions, buildPath: string): Promise { - throw new Error('Not implemented'); - } -} diff --git a/packages/expo-cli/src/commands/upload/IOSUploader.ts b/packages/expo-cli/src/commands/upload/IOSUploader.ts deleted file mode 100644 index 23deed69b6..0000000000 --- a/packages/expo-cli/src/commands/upload/IOSUploader.ts +++ /dev/null @@ -1,204 +0,0 @@ -import { ExpoConfig } from '@expo/config'; -import { UrlUtils } from '@expo/xdl'; -import chalk from 'chalk'; -import pick from 'lodash/pick'; - -import CommandError from '../../CommandError'; -import { authenticate, requestAppleIdCreds } from '../../appleApi'; -import { Context } from '../../credentials/context'; -import log from '../../log'; -import prompt, { Question } from '../../prompts'; -import BaseUploader, { PlatformOptions } from './BaseUploader'; -import { runFastlaneAsync } from './utils'; - -const PLATFORM = 'ios'; - -const APP_NAME_TOO_LONG_MSG = `An app name can't be longer than 30 characters.`; -const APP_NAME_QUESTION: Question = { - type: 'text', - name: 'appName', - message: 'How would you like to name your app?', - validate(appName: string): string | true { - if (!appName) { - return 'Empty app name is not valid.'; - } else if (appName.length > 30) { - return APP_NAME_TOO_LONG_MSG; - } else { - return true; - } - }, -}; - -export const LANGUAGES = [ - 'Brazilian Portuguese', - 'Danish', - 'Dutch', - 'English', - 'English_Australian', - 'English_CA', - 'English_UK', - 'Finnish', - 'French', - 'French_CA', - 'German', - 'Greek', - 'Indonesian', - 'Italian', - 'Japanese', - 'Korean', - 'Malay', - 'Norwegian', - 'Portuguese', - 'Russian', - 'Simplified Chinese', - 'Spanish', - 'Spanish_MX', - 'Swedish', - 'Thai', - 'Traditional Chinese', - 'Turkish', - 'Vietnamese', -]; - -export type IosPlatformOptions = PlatformOptions & { - appleId?: string; - appleIdPassword?: string; - appName: string; - language?: string; - appleTeamId?: string; - publicUrl?: string; - companyName?: string; -}; - -type AppleCreds = Pick; - -interface AppleIdCredentials { - appleId: string; - appleIdPassword: string; -} - -export default class IOSUploader extends BaseUploader { - static validateOptions(options: IosPlatformOptions): void { - if (options.language && !LANGUAGES.includes(options.language)) { - throw new Error( - `You must specify a supported language. Run expo upload:ios --help to see the list of supported languages.` - ); - } - if (options.publicUrl && !UrlUtils.isHttps(options.publicUrl)) { - throw new CommandError('INVALID_PUBLIC_URL', '--public-url must be a valid HTTPS URL.'); - } - } - - constructor(projectDir: string, public options: IosPlatformOptions) { - super(PLATFORM, projectDir, options); - } - - _ensureExperienceIsValid(exp: ExpoConfig): void { - if (!exp.ios?.bundleIdentifier) { - throw new Error(`You must specify an iOS bundle identifier in app.json.`); - } - } - - async _getPlatformSpecificOptions(): Promise<{ [key: string]: any }> { - const appleIdCredentials = await this._getAppleIdCredentials(); - const appleTeamId = await this._getAppleTeamId(appleIdCredentials); - const appName = await this._getAppName(); - const otherOptions = pick(this.options, ['language', 'sku', 'companyName']); - return { - ...appleIdCredentials, - appName, - ...otherOptions, - appleTeamId, - }; - } - - async _getAppleTeamId(appleIdCredentials: AppleIdCredentials): Promise { - const ctx = new Context(); - await ctx.init(this.projectDir); - let teamId; - if (ctx.hasProjectContext && ctx.manifest?.ios?.bundleIdentifier) { - const app = { - accountName: ctx.projectOwner, - projectName: ctx.manifest.slug, - bundleIdentifier: ctx.manifest?.ios?.bundleIdentifier, - }; - const credentials = await ctx.ios.getAppCredentials(app); - teamId = credentials?.credentials?.teamId; - } - - if (teamId) { - return teamId; - } else { - const { team } = await authenticate(appleIdCredentials); - return team.id; - } - } - - async _getAppleIdCredentials(): Promise { - return await requestAppleIdCreds({ - appleId: this.options.appleId, - appleIdPassword: this.options.appleIdPassword, - }); - } - - async _getAppName(): Promise { - const appName = this.options.appName || (this._exp && this._exp.name); - if (!appName || appName.length > 30) { - if (appName && appName.length > 30) { - log.error(APP_NAME_TOO_LONG_MSG); - } - return await this._askForAppName(); - } else { - return appName; - } - } - - async _askForAppName(): Promise { - const { appName } = await prompt(APP_NAME_QUESTION); - return appName; - } - - async _uploadToTheStore(platformData: IosPlatformOptions, buildPath: string): Promise { - const { fastlane } = this; - const { appleId, appleIdPassword, appName, language, appleTeamId, companyName } = platformData; - - const appleCreds = { appleId, appleIdPassword, appleTeamId, companyName }; - - log('Resolving the ITC team ID...'); - const { itc_team_id: itcTeamId } = await runFastlaneAsync( - fastlane.resolveItcTeamId, - [], - appleCreds - ); - log(`ITC team ID is ${itcTeamId}`); - const updatedAppleCreds = { - ...appleCreds, - itcTeamId, - }; - - log('Ensuring the app exists on App Store Connect, this may take a while...'); - try { - await runFastlaneAsync( - fastlane.appProduce, - [this._exp?.ios?.bundleIdentifier, appName, appleId, language], - updatedAppleCreds - ); - } catch (err) { - if (err.message.match(/You must provide a company name to use on the App Store/)) { - log.error( - 'You haven\'t uploaded any app to App Store yet. Please provide your company name with --company-name "COMPANY NAME"' - ); - } - throw err; - } - - log('Uploading the app to Testflight, hold tight...'); - await runFastlaneAsync(fastlane.pilotUpload, [buildPath, appleId], updatedAppleCreds); - - log( - `All done! You may want to go to App Store Connect (${chalk.underline( - 'https://appstoreconnect.apple.com' - )}) and share your build with your testers.` - ); - } -} diff --git a/packages/expo-cli/src/commands/upload/utils.ts b/packages/expo-cli/src/commands/upload/utils.ts deleted file mode 100644 index 9cac0f0e77..0000000000 --- a/packages/expo-cli/src/commands/upload/utils.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { ExponentTools } from '@expo/xdl'; - -const { spawnAsyncThrowError } = ExponentTools; - -export async function runFastlaneAsync( - program: string, - args: any, - { - appleId, - appleIdPassword, - appleTeamId, - itcTeamId, - companyName, - }: { - appleId?: string; - appleIdPassword?: string; - appleTeamId?: string; - itcTeamId?: string; - companyName?: string; - }, - pipeToLogger = false -): Promise<{ [key: string]: any }> { - const pipeToLoggerOptions: any = pipeToLogger - ? { pipeToLogger: { stdout: true } } - : { stdio: [0, 1, 'pipe'] }; - - const fastlaneData = - appleId && appleIdPassword - ? { - FASTLANE_USER: appleId, - FASTLANE_PASSWORD: appleIdPassword, - FASTLANE_DONT_STORE_PASSWORD: '1', - FASTLANE_TEAM_ID: appleTeamId, - ...(itcTeamId && { FASTLANE_ITC_TEAM_ID: itcTeamId }), - ...(companyName && { PRODUCE_COMPANY_NAME: companyName }), - } - : {}; - - const env = { - ...process.env, - ...fastlaneData, - }; - - const spawnOptions: ExponentTools.AsyncSpawnOptions = { - ...pipeToLoggerOptions, - env, - }; - - const { stderr } = await spawnAsyncThrowError(program, args, spawnOptions); - - const res = JSON.parse(stderr); - if (res.result !== 'failure') { - return res; - } else { - let message = - res.reason !== 'Unknown reason' - ? res.reason - : res.rawDump?.message ?? 'Unknown error when running fastlane'; - message = `${message}${ - res?.rawDump?.backtrace - ? `\n${res.rawDump.backtrace.map((i: string) => ` ${i}`).join('\n')}` - : '' - }`; - throw new Error(message); - } -} diff --git a/packages/expo-cli/src/commands/utils/TerminalLink.ts b/packages/expo-cli/src/commands/utils/TerminalLink.ts index 86531480d3..0be4b3ec01 100644 --- a/packages/expo-cli/src/commands/utils/TerminalLink.ts +++ b/packages/expo-cli/src/commands/utils/TerminalLink.ts @@ -44,6 +44,14 @@ export function learnMore(url: string): string { }); } +export function linkedText(text: string, url: string): string { + return terminalLink(text, url, { + fallback: (text, url) => { + return `${text} ${log.chalk.dim.underline(url)}`; + }, + }); +} + export function transporterAppLink() { return fallbackToTextAndUrl( 'Transporter.app',