Skip to content

Commit

Permalink
Merge pull request #4492 from Shopify/login-on-init
Browse files Browse the repository at this point in the history
Authenticate on app init command
  • Loading branch information
isaacroldan authored Sep 25, 2024
2 parents fcf7f39 + ad6d0ab commit f0318f3
Show file tree
Hide file tree
Showing 30 changed files with 451 additions and 398 deletions.
6 changes: 6 additions & 0 deletions docs-shopify.dev/commands/interfaces/app-init.interface.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
// This is an autogenerated file. Don't edit this file manually.
export interface appinit {
/**
* The Client ID of your app. Use this to automatically link your new project to an existing app. Using this flag avoids the app selection prompt.
* @environment SHOPIFY_FLAG_CLIENT_ID
*/
'--client-id <value>'?: string

/**
* Which flavor of the given template to use.
* @environment SHOPIFY_FLAG_TEMPLATE_FLAVOR
Expand Down
11 changes: 10 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -1498,6 +1498,15 @@
"name": "appinit",
"description": "",
"members": [
{
"filePath": "docs-shopify.dev/commands/interfaces/app-init.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--client-id <value>",
"value": "string",
"description": "The Client ID of your app. Use this to automatically link your new project to an existing app. Using this flag avoids the app selection prompt.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_CLIENT_ID"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-init.interface.ts",
"syntaxKind": "PropertySignature",
Expand Down Expand Up @@ -1562,7 +1571,7 @@
"environmentValue": "SHOPIFY_FLAG_PATH"
}
],
"value": "export interface appinit {\n /**\n * Which flavor of the given template to use.\n * @environment SHOPIFY_FLAG_TEMPLATE_FLAVOR\n */\n '--flavor <value>'?: string\n\n /**\n * \n * @environment SHOPIFY_FLAG_NAME\n */\n '-n, --name <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * \n * @environment SHOPIFY_FLAG_PACKAGE_MANAGER\n */\n '-d, --package-manager <value>'?: string\n\n /**\n * \n * @environment SHOPIFY_FLAG_PATH\n */\n '-p, --path <value>'?: string\n\n /**\n * The app template. Accepts one of the following:\n - <remix|none>\n - Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify/<repository>/[subpath]#[branch]\n * @environment SHOPIFY_FLAG_TEMPLATE\n */\n '--template <value>'?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
"value": "export interface appinit {\n /**\n * The Client ID of your app. Use this to automatically link your new project to an existing app. Using this flag avoids the app selection prompt.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * Which flavor of the given template to use.\n * @environment SHOPIFY_FLAG_TEMPLATE_FLAVOR\n */\n '--flavor <value>'?: string\n\n /**\n * \n * @environment SHOPIFY_FLAG_NAME\n */\n '-n, --name <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * \n * @environment SHOPIFY_FLAG_PACKAGE_MANAGER\n */\n '-d, --package-manager <value>'?: string\n\n /**\n * \n * @environment SHOPIFY_FLAG_PATH\n */\n '-p, --path <value>'?: string\n\n /**\n * The app template. Accepts one of the following:\n - <remix|none>\n - Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify/<repository>/[subpath]#[branch]\n * @environment SHOPIFY_FLAG_TEMPLATE\n */\n '--template <value>'?: string\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,8 @@
"pin-github-action",
"react",
"@graphql-codegen/near-operation-file-preset",
"graphql-codegen-typescript-operation-types"
"graphql-codegen-typescript-operation-types",
"esbuild"
],
"ignore": [
"configurations/vite.config.ts"
Expand Down Expand Up @@ -191,8 +192,7 @@
],
"project": "**/*.ts!",
"ignoreDependencies": [
"@ast-grep/napi",
"@parcel/watcher"
"@ast-grep/napi"
],
"vite": {
"config": [
Expand Down
1 change: 0 additions & 1 deletion packages/app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,6 @@
"@luckycatfactory/esbuild-graphql-loader": "3.8.1",
"@oclif/core": "3.26.5",
"@shopify/cli-kit": "3.67.0",
"@shopify/create-app": "3.67.0",
"@shopify/theme": "3.67.0",
"@shopify/function-runner": "4.1.1",
"@shopify/plugin-cloudflare": "3.67.0",
Expand Down
122 changes: 120 additions & 2 deletions packages/app/src/cli/commands/app/init.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,120 @@
// The init command is re-exported from @shopify/create-app to reuse all the logic from there
export {default} from '@shopify/create-app/commands/init'
import initPrompt, {visibleTemplates} from '../../prompts/init/init.js'
import initService from '../../services/init/init.js'
import {selectDeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {appFromId, selectOrg} from '../../services/context.js'
import {selectOrCreateApp} from '../../services/dev/select-app.js'
import AppCommand from '../../utilities/app-command.js'
import {validateFlavorValue, validateTemplateValue} from '../../services/init/validate.js'
import {OrganizationApp} from '../../models/organization.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'

export default class Init extends AppCommand {
static summary?: string | undefined = 'Create a new app project'

static flags = {
...globalFlags,
name: Flags.string({
char: 'n',
env: 'SHOPIFY_FLAG_NAME',
hidden: false,
}),
path: Flags.string({
char: 'p',
env: 'SHOPIFY_FLAG_PATH',
parse: async (input) => resolvePath(input),
default: async () => cwd(),
hidden: false,
}),
template: Flags.string({
description: `The app template. Accepts one of the following:
- <${visibleTemplates.join('|')}>
- Any GitHub repo with optional branch and subpath, e.g., https://github.com/Shopify/<repository>/[subpath]#[branch]`,
env: 'SHOPIFY_FLAG_TEMPLATE',
}),
flavor: Flags.string({
description: 'Which flavor of the given template to use.',
env: 'SHOPIFY_FLAG_TEMPLATE_FLAVOR',
}),
'package-manager': Flags.string({
char: 'd',
env: 'SHOPIFY_FLAG_PACKAGE_MANAGER',
hidden: false,
options: ['npm', 'yarn', 'pnpm', 'bun'],
}),
local: Flags.boolean({
char: 'l',
env: 'SHOPIFY_FLAG_LOCAL',
default: false,
hidden: true,
}),
'client-id': Flags.string({
hidden: false,
description:
'The Client ID of your app. Use this to automatically link your new project to an existing app. Using this flag avoids the app selection prompt.',
env: 'SHOPIFY_FLAG_CLIENT_ID',
exclusive: ['config'],
}),
}

async run(): Promise<void> {
const {flags} = await this.parse(Init)

validateTemplateValue(flags.template)
validateFlavorValue(flags.template, flags.flavor)

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()

let selectedApp: OrganizationApp
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})
} else {
renderText({text: "\nWelcome. Let's get started by linking this new project to an app in your organization."})
const org = await selectOrg()
const {organization, apps, hasMorePages} = await developerPlatformClient.orgAndApps(org.id)
selectedApp = await selectOrCreateApp(name, apps, hasMorePages, organization, developerPlatformClient)
}

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

if (promptAnswers.globalCLIResult.install) {
await installGlobalShopifyCLI(inferredPackageManager)
}

await addPublicMetadata(() => ({
cmd_create_app_template: promptAnswers.templateType,
cmd_create_app_template_url: promptAnswers.template,
}))

const platformClient = selectedApp.developerPlatformClient ?? developerPlatformClient

await initService({
name: selectedApp.title,
app: selectedApp,
packageManager: inferredPackageManager,
template: promptAnswers.template,
local: flags.local,
directory: flags.path,
useGlobalCLI: promptAnswers.globalCLIResult.alreadyInstalled || promptAnswers.globalCLIResult.install,
developerPlatformClient: platformClient,
postCloneActions: {
removeLockfilesFromGitignore: promptAnswers.templateType !== 'custom',
},
})
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import init, {InitOptions} from './init.js'
import {describe, expect, vi, test, beforeEach} from 'vitest'
import {renderSelectPrompt, renderText, renderTextPrompt} from '@shopify/cli-kit/node/ui'
import {renderSelectPrompt} from '@shopify/cli-kit/node/ui'
import {installGlobalCLIPrompt} from '@shopify/cli-kit/node/is-global'

vi.mock('@shopify/cli-kit/node/ui')
Expand All @@ -12,56 +12,14 @@ describe('init', () => {
beforeEach(() => {
vi.mocked(installGlobalCLIPrompt).mockResolvedValue(globalCLIResult)
})
test('when name is not passed', async () => {
const answers = {
name: 'app',
}
const options: InitOptions = {template: 'template', directory: '/'}

// Given
vi.mocked(renderTextPrompt).mockResolvedValueOnce(answers.name)

// When
const got = await init(options)

// Then
expect(renderText).toHaveBeenCalledWith({
text: '\nWelcome. Let’s get started by naming your app project. You can change it later.',
})
expect(renderTextPrompt).toHaveBeenCalledWith({
message: 'Your project name?',
defaultValue: expect.stringMatching(/^\w+-\w+-app$/),
validate: expect.any(Function),
})
expect(got).toEqual({...options, ...answers, templateType: 'custom', globalCLIResult})
})

test('when name is passed', async () => {
const answers = {
template: 'https://github.com/Shopify/shopify-app-template-remix',
}
const options: InitOptions = {name: 'app', directory: '/'}

// When
const got = await init(options)

// Then
expect(renderText).toHaveBeenCalledWith({
text: '\nWelcome. Let’s get started by choosing a template for your app project.',
})
expect(renderTextPrompt).not.toHaveBeenCalled()
expect(got).toEqual({...options, ...answers, templateType: 'custom', globalCLIResult})
})

test('it renders the label for the template options', async () => {
const answers = {
name: 'app',
template: 'https://github.com/Shopify/shopify-app-template-none',
}
const options: InitOptions = {directory: '/'}
const options: InitOptions = {}

// Given
vi.mocked(renderTextPrompt).mockResolvedValueOnce(answers.name)
vi.mocked(renderSelectPrompt).mockResolvedValueOnce('none')

// When
Expand All @@ -81,13 +39,11 @@ describe('init', () => {

test('it renders branches for templates that have them', async () => {
const answers = {
name: 'app',
template: 'https://github.com/Shopify/shopify-app-template-remix#javascript',
}
const options: InitOptions = {directory: '/'}
const options: InitOptions = {}

// Given
vi.mocked(renderTextPrompt).mockResolvedValueOnce(answers.name)
vi.mocked(renderSelectPrompt).mockResolvedValueOnce('remix')
vi.mocked(renderSelectPrompt).mockResolvedValueOnce('javascript')

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,12 @@
import {generateRandomNameForSubdirectory} from '@shopify/cli-kit/node/fs'
import {InstallGlobalCLIPromptResult, installGlobalCLIPrompt} from '@shopify/cli-kit/node/is-global'
import {renderText, renderSelectPrompt, renderTextPrompt} from '@shopify/cli-kit/node/ui'
import {renderSelectPrompt} from '@shopify/cli-kit/node/ui'

export interface InitOptions {
name?: string
template?: string
flavor?: string
directory: string
}

interface InitOutput {
name: string
template: string
// e.g. 'remix'
templateType: PredefinedTemplate | 'custom'
Expand All @@ -21,6 +17,7 @@ interface TemplateBranch {
branch: string
label: string
}

interface Template {
url: string
label?: string
Expand Down Expand Up @@ -72,42 +69,14 @@ export const visibleTemplates = allTemplates.filter((key) => templates[key].visi
const templateOptionsInOrder = ['remix', 'none'] as const

const init = async (options: InitOptions): Promise<InitOutput> => {
let name = options.name
let template = options.template
const flavor = options.flavor

const defaults = {
name: await generateRandomNameForSubdirectory({suffix: 'app', directory: options.directory}),
template: templates.remix.url,
} as const

let welcomed = false

if (!name) {
renderText({text: '\nWelcome. Let’s get started by naming your app project. You can change it later.'})
welcomed = true
name = await renderTextPrompt({
message: 'Your project name?',
defaultValue: defaults.name,
validate: (value) => {
if (value.length === 0) {
return "App name can't be empty"
}
if (value.length > 30) {
return 'Enter a shorter name (30 character max.)'
}
if (value.toLowerCase().includes('shopify')) {
return "App name can't include the word 'shopify'"
}
},
})
}

if (!template) {
if (!welcomed) {
renderText({text: '\nWelcome. Let’s get started by choosing a template for your app project.'})
welcomed = true
}
template = await renderSelectPrompt({
choices: templateOptionsInOrder.map((key) => {
return {
Expand All @@ -122,7 +91,6 @@ const init = async (options: InitOptions): Promise<InitOutput> => {

const answers: InitOutput = {
...options,
name,
template,
templateType: isPredefinedTemplate(template) ? template : 'custom',
globalCLIResult: {install: false, alreadyInstalled: false},
Expand Down
9 changes: 8 additions & 1 deletion packages/app/src/cli/services/app/config/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ import {PackageManager} from '@shopify/cli-kit/node/node-package-manager'
export interface LinkOptions {
directory: string
apiKey?: string
appId?: string
organizationId?: string
configName?: string
baseConfigName?: string
developerPlatformClient?: DeveloperPlatformClient
Expand Down Expand Up @@ -113,7 +115,12 @@ async function selectOrCreateRemoteAppToLinkTo(options: LinkOptions): Promise<{

if (options.apiKey) {
// Remote API Key provided by the caller, so use that app specifically
const remoteApp = await appFromId({apiKey: options.apiKey, developerPlatformClient})
const remoteApp = await appFromId({
apiKey: options.apiKey,
id: options.appId,
developerPlatformClient,
organizationId: options.organizationId,
})
if (!remoteApp) {
const errorMessage = InvalidApiKeyErrorMessage(options.apiKey)
throw new AbortError(errorMessage.message, errorMessage.tryMessage)
Expand Down
2 changes: 0 additions & 2 deletions packages/app/src/cli/services/dev/select-app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import {Organization, MinimalOrganizationApp, OrganizationApp} from '../../model
import {getCachedCommandInfo, setCachedCommandTomlPreference} from '../local-storage.js'
import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js'
import {AppConfigurationFileName} from '../../models/app/loader.js'
import {outputInfo} from '@shopify/cli-kit/node/output'

/**
* Select an app from env, list or create a new one:
Expand All @@ -29,7 +28,6 @@ export async function selectOrCreateApp(
): Promise<OrganizationApp> {
let createNewApp = apps.length === 0
if (!createNewApp) {
outputInfo(`\nBefore proceeding, your project needs to be associated with an app.\n`)
createNewApp = await createAsNewAppPrompt()
}
if (createNewApp) {
Expand Down
Loading

0 comments on commit f0318f3

Please sign in to comment.