Skip to content
Draft
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
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,18 @@ export interface appconfiglink {
*/
'-c, --config <value>'?: string

/**
* The name of the app configuration file to create or overwrite.
* @environment SHOPIFY_FLAG_APP_CONFIG_FILE_NAME
*/
'--file-name <value>'?: string

/**
* Overwrite an existing configuration file without prompting.
* @environment SHOPIFY_FLAG_FORCE
*/
'--force'?: ''

/**
* Disable color output.
* @environment SHOPIFY_FLAG_NO_COLOR
Expand Down
20 changes: 19 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,24 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_CLIENT_ID"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-config-link.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--file-name <value>",
"value": "string",
"description": "The name of the app configuration file to create or overwrite.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_APP_CONFIG_FILE_NAME"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-config-link.interface.ts",
"syntaxKind": "PropertySignature",
"name": "--force",
"value": "''",
"description": "Overwrite an existing configuration file without prompting.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_FORCE"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/app-config-link.interface.ts",
"syntaxKind": "PropertySignature",
Expand Down Expand Up @@ -437,7 +455,7 @@
"environmentValue": "SHOPIFY_FLAG_APP_CONFIG"
}
],
"value": "export interface appconfiglink {\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
"value": "export interface appconfiglink {\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * The name of the app configuration file to create or overwrite.\n * @environment SHOPIFY_FLAG_APP_CONFIG_FILE_NAME\n */\n '--file-name <value>'?: string\n\n /**\n * Overwrite an existing configuration file without prompting.\n * @environment SHOPIFY_FLAG_FORCE\n */\n '--force'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"appconfigpull": {
Expand Down
42 changes: 42 additions & 0 deletions packages/app/src/cli/commands/app/config/link.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import ConfigLink from './link.js'
import link from '../../../services/app/config/link.js'
import {linkedAppContext} from '../../../services/app-context.js'
import {testAppLinked, testOrganizationApp} from '../../../models/app/app.test-data.js'
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
import {describe, expect, test, vi} from 'vitest'

vi.mock('../../../services/app/config/link.js')
vi.mock('../../../services/app-context.js')

describe('app config link command', () => {
test('accepts --client-id with --file-name to link a specific app to a specific config file', async () => {
await inTemporaryDirectory(async (tmp) => {
const app = testAppLinked()
vi.mocked(link).mockResolvedValue({
remoteApp: testOrganizationApp(),
configFileName: 'shopify.app.staging.toml',
configuration: app.configuration,
})
vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited<ReturnType<typeof linkedAppContext>>)

await ConfigLink.run(
['--path', tmp, '--client-id', 'api-key', '--file-name', 'staging', '--force'],
import.meta.url,
)

expect(link).toHaveBeenCalledWith({
directory: tmp,
apiKey: 'api-key',
configName: undefined,
fileName: 'staging',
force: true,
})
expect(linkedAppContext).toHaveBeenCalledWith({
directory: tmp,
clientId: undefined,
forceRelink: false,
userProvidedConfigName: 'shopify.app.staging.toml',
})
})
})
})
15 changes: 15 additions & 0 deletions packages/app/src/cli/commands/app/config/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import {linkedAppContext} from '../../../services/app-context.js'
import link, {LinkOptions} from '../../../services/app/config/link.js'
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {Flags} from '@oclif/core'

export default class ConfigLink extends AppLinkedCommand {
static summary = 'Fetch your app configuration from the Developer Dashboard.'
Expand All @@ -17,6 +18,18 @@ export default class ConfigLink extends AppLinkedCommand {
static flags = {
...globalFlags,
...appFlags,
'file-name': Flags.string({
hidden: false,
description: 'The name of the app configuration file to create or overwrite.',
env: 'SHOPIFY_FLAG_APP_CONFIG_FILE_NAME',
exclusive: ['config'],
}),
force: Flags.boolean({
hidden: false,
description: 'Overwrite an existing configuration file without prompting.',
env: 'SHOPIFY_FLAG_FORCE',
default: false,
}),
}

public async run(): Promise<AppLinkedCommandOutput> {
Expand All @@ -26,6 +39,8 @@ export default class ConfigLink extends AppLinkedCommand {
directory: flags.path,
apiKey: flags['client-id'],
configName: flags.config,
fileName: flags['file-name'],
force: flags.force,
}

const result = await link(options)
Expand Down
70 changes: 70 additions & 0 deletions packages/app/src/cli/services/app/config/link.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,76 @@ describe('link', () => {
})
})

test('does not ask for a name when a file name is provided as a flag', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
const developerPlatformClient = buildDeveloperPlatformClient()
const options: LinkOptions = {
directory: tmp,
fileName: 'staging',
developerPlatformClient,
}
await mockLoadOpaqueAppWithApp(tmp)
vi.mocked(fetchOrCreateOrganizationApp).mockResolvedValue(mockRemoteApp({developerPlatformClient}))

// When
const {configFileName} = await link(options)

// Then
expect(selectConfigName).not.toHaveBeenCalled()
expect(configFileName).toBe('shopify.app.staging.toml')
expect(fileExistsSync(joinPath(tmp, 'shopify.app.staging.toml'))).toBeTruthy()
})
})

test('throws when a file name is provided for an existing file without force', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
const developerPlatformClient = buildDeveloperPlatformClient()
const options: LinkOptions = {
directory: tmp,
fileName: 'staging',
developerPlatformClient,
}
writeFileSync(joinPath(tmp, 'shopify.app.staging.toml'), 'client_id = "12345"')
await mockLoadOpaqueAppWithApp(tmp)
vi.mocked(fetchOrCreateOrganizationApp).mockResolvedValue(mockRemoteApp({developerPlatformClient}))

// When
const result = link(options)

// Then
await expect(result).rejects.toThrow(/Configuration file shopify\.app\.staging\.toml already exists/)
expect(selectConfigName).not.toHaveBeenCalled()
})
})

test('overwrites an existing file name when force is provided', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
const developerPlatformClient = buildDeveloperPlatformClient()
const options: LinkOptions = {
directory: tmp,
fileName: 'staging',
force: true,
developerPlatformClient,
}
writeFileSync(joinPath(tmp, 'shopify.app.staging.toml'), 'name = "old app"')
await mockLoadOpaqueAppWithApp(tmp)
vi.mocked(fetchOrCreateOrganizationApp).mockResolvedValue(mockRemoteApp({developerPlatformClient}))

// When
const {configFileName} = await link(options)

// Then
const content = await readFile(joinPath(tmp, 'shopify.app.staging.toml'))
expect(selectConfigName).not.toHaveBeenCalled()
expect(configFileName).toBe('shopify.app.staging.toml')
expect(content).toContain('client_id = "12345"')
expect(content).not.toContain('old app')
})
})

test('does not ask for a name when the selected app is already linked', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
Expand Down
15 changes: 15 additions & 0 deletions packages/app/src/cli/services/app/config/link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {loadLocalExtensionsSpecifications} from '../../../models/extensions/load
import {renderSuccess} from '@shopify/cli-kit/node/ui'
import {formatPackageManagerCommand} from '@shopify/cli-kit/node/output'
import {deepMergeObjects, isEmpty} from '@shopify/cli-kit/common/object'
import {fileExists} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'
import {AbortError} from '@shopify/cli-kit/node/error'
import {PackageManager} from '@shopify/cli-kit/node/node-package-manager'
Expand All @@ -36,6 +37,8 @@ export interface LinkOptions {
appId?: string
organizationId?: string
configName?: string
fileName?: string
force?: boolean
developerPlatformClient?: DeveloperPlatformClient
isNewApp?: boolean
}
Expand Down Expand Up @@ -293,6 +296,18 @@ async function loadConfigurationFileName(
appDirectory?: string
},
): Promise<AppConfigurationFileName> {
if (options.fileName) {
const fileName = getAppConfigurationFileName(options.fileName)
const appDirectory = localAppInfo.appDirectory ?? options.directory
if (!options.force && (await fileExists(joinPath(appDirectory, fileName)))) {
throw new AbortError(
`Configuration file ${fileName} already exists.`,
'Run the command with --force to overwrite it.',
)
}
return fileName
}

if (options.configName) {
return getAppConfigurationFileName(options.configName)
}
Expand Down
6 changes: 5 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -241,11 +241,15 @@ Fetch your app configuration from the Developer Dashboard.

```
USAGE
$ shopify app config link [--client-id <value> | -c <value>] [--no-color] [--path <value>] [--reset | ] [--verbose]
$ shopify app config link [--client-id <value> | -c <value>] [--file-name <value> | ] [--force] [--no-color]
[--path <value>] [--reset | ] [--verbose]

FLAGS
-c, --config=<value> [env: SHOPIFY_FLAG_APP_CONFIG] The name of the app configuration.
--client-id=<value> [env: SHOPIFY_FLAG_CLIENT_ID] The Client ID of your app.
--file-name=<value> [env: SHOPIFY_FLAG_APP_CONFIG_FILE_NAME] The name of the app configuration file to create or
overwrite.
--force [env: SHOPIFY_FLAG_FORCE] Overwrite an existing configuration file without prompting.
--no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output.
--path=<value> [env: SHOPIFY_FLAG_PATH] The path to your app directory.
--reset [env: SHOPIFY_FLAG_RESET] Reset all your settings.
Expand Down
20 changes: 20 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -461,6 +461,26 @@
"name": "config",
"type": "option"
},
"file-name": {
"description": "The name of the app configuration file to create or overwrite.",
"env": "SHOPIFY_FLAG_APP_CONFIG_FILE_NAME",
"exclusive": [
"config"
],
"hasDynamicHelp": false,
"hidden": false,
"multiple": false,
"name": "file-name",
"type": "option"
},
"force": {
"allowNo": false,
"description": "Overwrite an existing configuration file without prompting.",
"env": "SHOPIFY_FLAG_FORCE",
"hidden": false,
"name": "force",
"type": "boolean"
},
"no-color": {
"allowNo": false,
"description": "Disable color output.",
Expand Down
Loading