Skip to content

Commit

Permalink
Merge pull request #4668 from Shopify/updates-to-init-linking
Browse files Browse the repository at this point in the history
Defer remote app creation during `app init` to include all default creation options.
  • Loading branch information
isaacroldan authored Oct 18, 2024
2 parents 7098eff + 2c6fbd4 commit 7b8165f
Show file tree
Hide file tree
Showing 2 changed files with 85 additions and 31 deletions.
94 changes: 68 additions & 26 deletions packages/app/src/cli/commands/app/init.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import initPrompt, {visibleTemplates} from '../../prompts/init/init.js'
import initService from '../../services/init/init.js'
import {selectDeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {DeveloperPlatformClient, selectDeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {appFromId, selectOrg} from '../../services/context.js'
import {selectOrCreateApp} from '../../services/dev/select-app.js'
import AppCommand, {AppCommandOutput} from '../../utilities/app-command.js'
import {validateFlavorValue, validateTemplateValue} from '../../services/init/validate.js'
import {OrganizationApp} from '../../models/organization.js'
import {loadApp} from '../../models/app/loader.js'
import {loadLocalExtensionsSpecifications} from '../../models/extensions/load-specifications.js'
import {MinimalOrganizationApp, Organization, OrganizationApp} from '../../models/organization.js'
import {appNamePrompt, createAsNewAppPrompt, selectAppPrompt} from '../../prompts/dev.js'
import {searchForAppsByNameFactory} from '../../services/dev/prompt-helpers.js'
import {linkedAppContext} from '../../services/app-context.js'
import {Flags} from '@oclif/core'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {resolvePath, cwd} from '@shopify/cli-kit/node/path'
import {addPublicMetadata} from '@shopify/cli-kit/node/metadata'

import {installGlobalShopifyCLI} from '@shopify/cli-kit/node/is-global'
import {generateRandomNameForSubdirectory} from '@shopify/cli-kit/node/fs'
import {renderText} from '@shopify/cli-kit/node/ui'
import {inferPackageManager} from '@shopify/cli-kit/node/node-package-manager'
import {AbortError} from '@shopify/cli-kit/node/error'

export default class Init extends AppCommand {
static summary?: string | undefined = 'Create a new app project'
Expand Down Expand Up @@ -75,25 +75,31 @@ export default class Init extends AppCommand {
const inferredPackageManager = inferPackageManager(flags['package-manager'])
const name = flags.name ?? (await generateRandomNameForSubdirectory({suffix: 'app', directory: flags.path}))

// Authenticate and select organization and app
const developerPlatformClient = selectDeveloperPlatformClient()
// Force user authentication before prompting.
let developerPlatformClient = selectDeveloperPlatformClient()
await developerPlatformClient.session()

let selectedApp: OrganizationApp
const promptAnswers = await initPrompt({
template: flags.template,
flavor: flags.flavor,
})

let selectAppResult: SelectAppOrNewAppNameResult
let appName: string
if (flags['client-id']) {
// If a client-id is provided we don't need to prompt the user and can link directly to that app.
selectedApp = await appFromId({apiKey: flags['client-id'], developerPlatformClient})
const selectedApp = await appFromId({apiKey: flags['client-id'], developerPlatformClient})
appName = selectedApp.title
developerPlatformClient = selectedApp.developerPlatformClient ?? developerPlatformClient
selectAppResult = {result: 'existing', app: selectedApp}
} else {
renderText({text: "\nWelcome. Let's get started by linking this new project to an app in your organization."})
const org = await selectOrg()
developerPlatformClient = selectDeveloperPlatformClient({organization: org})
const {organization, apps, hasMorePages} = await developerPlatformClient.orgAndApps(org.id)
selectedApp = await selectOrCreateApp(name, apps, hasMorePages, organization, developerPlatformClient)
selectAppResult = await selectAppOrNewAppName(name, apps, hasMorePages, organization, developerPlatformClient)
appName = selectAppResult.result === 'new' ? selectAppResult.name : selectAppResult.app.title
}

const promptAnswers = await initPrompt({
template: flags.template,
flavor: flags.flavor,
})

if (promptAnswers.globalCLIResult.install) {
await installGlobalShopifyCLI(inferredPackageManager)
}
Expand All @@ -103,30 +109,66 @@ export default class Init extends AppCommand {
cmd_create_app_template_url: promptAnswers.template,
}))

const platformClient = selectedApp.developerPlatformClient ?? developerPlatformClient

const result = await initService({
name: selectedApp.title,
app: selectedApp,
name: appName,
selectedAppOrNameResult: selectAppResult,
packageManager: inferredPackageManager,
template: promptAnswers.template,
local: flags.local,
directory: flags.path,
useGlobalCLI: promptAnswers.globalCLIResult.alreadyInstalled || promptAnswers.globalCLIResult.install,
developerPlatformClient: platformClient,
developerPlatformClient,
postCloneActions: {
removeLockfilesFromGitignore: promptAnswers.templateType !== 'custom',
},
})

const specifications = await loadLocalExtensionsSpecifications()

const app = await loadApp({
specifications,
const {app} = await linkedAppContext({
directory: result.outputDirectory,
clientId: undefined,
forceRelink: false,
userProvidedConfigName: undefined,
unsafeReportMode: false,
})

return {app}
}
}

export type SelectAppOrNewAppNameResult =
| {
result: 'new'
name: string
org: Organization
}
| {
result: 'existing'
app: OrganizationApp
}

/**
* This method returns either an existing app or a new app name and the data necessary to create it.
* But doesn't create the app yet, the app creation is deferred and is responsibility of the caller.
*/
async function selectAppOrNewAppName(
localAppName: string,
apps: MinimalOrganizationApp[],
hasMorePages: boolean,
org: Organization,
developerPlatformClient: DeveloperPlatformClient,
): Promise<SelectAppOrNewAppNameResult> {
let createNewApp = apps.length === 0
if (!createNewApp) {
createNewApp = await createAsNewAppPrompt()
}
if (createNewApp) {
const name = await appNamePrompt(localAppName)
return {result: 'new', name, org}
} else {
const app = await selectAppPrompt(searchForAppsByNameFactory(developerPlatformClient, org.id), apps, hasMorePages)

const fullSelectedApp = await developerPlatformClient.appFromId(app)
if (!fullSelectedApp) throw new AbortError(`App with id ${app.id} not found`)
return {result: 'existing', app: fullSelectedApp}
}
}
22 changes: 17 additions & 5 deletions packages/app/src/cli/services/init/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ import cleanup from './template/cleanup.js'
import link from '../app/config/link.js'
import {OrganizationApp} from '../../models/organization.js'
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {loadApp} from '../../models/app/loader.js'
import {SelectAppOrNewAppNameResult} from '../../commands/app/init.js'
import {
findUpAndReadPackageJson,
lockfiles,
Expand Down Expand Up @@ -34,7 +36,7 @@ import {LocalStorage} from '@shopify/cli-kit/node/local-storage'

interface InitOptions {
name: string
app: OrganizationApp
selectedAppOrNameResult: SelectAppOrNewAppNameResult
directory: string
template: string
packageManager: PackageManager
Expand Down Expand Up @@ -193,13 +195,23 @@ async function init(options: InitOptions) {
await moveFile(templateScaffoldDir, outputDirectory)
})

// Link the new project to the selected App
let app: OrganizationApp
if (options.selectedAppOrNameResult.result === 'new') {
// Load the local app to get the creation options. No need for specs since we only care about Creation Options.
const localApp = await loadApp({specifications: [], directory: outputDirectory, userProvidedConfigName: undefined})
const org = options.selectedAppOrNameResult.org
app = await options.developerPlatformClient.createApp(org, options.name, localApp.creationDefaultOptions())
} else {
app = options.selectedAppOrNameResult.app
}

// Link the new project to the selected/created App
await link(
{
directory: outputDirectory,
apiKey: options.app.apiKey,
appId: options.app.id,
organizationId: options.app.organizationId,
apiKey: app.apiKey,
appId: app.id,
organizationId: app.organizationId,
configName: 'shopify.app.toml',
developerPlatformClient: options.developerPlatformClient,
},
Expand Down

0 comments on commit 7b8165f

Please sign in to comment.