diff --git a/.changeset/json-app-channel-config.md b/.changeset/json-app-channel-config.md new file mode 100644 index 00000000000..b6a9d8f0789 --- /dev/null +++ b/.changeset/json-app-channel-config.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli': major +--- + +Return a success status and absolute file path in `app import channel-config --json`. diff --git a/packages/app/src/cli/commands/app/import/channel-config.test.ts b/packages/app/src/cli/commands/app/import/channel-config.test.ts new file mode 100644 index 00000000000..3b5e724413f --- /dev/null +++ b/packages/app/src/cli/commands/app/import/channel-config.test.ts @@ -0,0 +1,69 @@ +import ImportChannelConfig from './channel-config.js' +import {linkedAppContext} from '../../../services/app-context.js' +import {importChannelConfigJsonOutputSchema} from '../../../services/import-channel-config/types.js' +import { + CHANNEL_SPEC_DIRECTORY, + CHANNEL_SPEC_EXTENSION_DIRECTORY, +} from '../../../services/import-channel-config/import.js' +import {testAppLinked, testDeveloperPlatformClient, testOrganizationApp} from '../../../models/app/app.test-data.js' +import {Config} from '@oclif/core' +import {inTemporaryDirectory, readFile} from '@shopify/cli-kit/node/fs' +import {dirname, joinPath} from '@shopify/cli-kit/node/path' +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' +import {expect, test, vi} from 'vitest' +import {fileURLToPath} from 'node:url' + +vi.mock('../../../services/app-context.js') + +const TOML = 'handle = "example"\nlabel = "Café channel"\n' +const warning = {code: 'missing_countries', message: 'Add a countries section.'} + +test('imports real files and writes one JSON document with warning events on stderr', async () => { + await inTemporaryDirectory(async (tmp) => { + const app = testAppLinked({directory: tmp}) + const developerPlatformClient = testDeveloperPlatformClient({ + channelSpecExport: async () => ({ + ok: true, + status: 200, + body: {handle: 'example', filename: 'example.toml', toml: TOML, warnings: [{...warning, internal: 'private'}]}, + }), + }) + vi.mocked(linkedAppContext).mockResolvedValue({ + app, + remoteApp: testOrganizationApp(), + developerPlatformClient, + } as unknown as Awaited>) + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await runCommand(['--path', tmp, '--json']) + const path = joinPath(tmp, CHANNEL_SPEC_DIRECTORY, 'example.toml') + const result = JSON.parse(stdout()) + expect(result).toEqual({ + status: 'success', + handle: 'example', + filename: 'example.toml', + path, + toml: TOML, + warnings: [warning], + }) + expect(() => + importChannelConfigJsonOutputSchema.encode({...result, path: joinPath(CHANNEL_SPEC_DIRECTORY, 'example.toml')}), + ).toThrow() + expect(() => importChannelConfigJsonOutputSchema.encode({...result, internal: 'private'})).toThrow() + expect(() => + importChannelConfigJsonOutputSchema.encode({...result, warnings: [{...warning, internal: 'private'}]}), + ).toThrow() + expect(JSON.parse(stderr())).toMatchObject({type: 'diagnostic', level: 'warning', message: warning.message}) + await expect(readFile(path)).resolves.toBe(TOML) + await expect(readFile(joinPath(tmp, CHANNEL_SPEC_EXTENSION_DIRECTORY, 'shopify.extension.toml'))).resolves.toBe( + 'name = "Channel config"\ntype = "channel_config"\nhandle = "channel-config"\n', + ) + }) + }) +}) + +async function runCommand(argv: string[]) { + const config = await Config.load({root: joinPath(dirname(fileURLToPath(import.meta.url)), '../../../../..')}) + // The installed CLI bundles this source package instead of loading it as a custom plugin. + config.plugins.delete('@shopify/app') + return ImportChannelConfig.run(argv, config) +} diff --git a/packages/app/src/cli/commands/app/import/channel-config.ts b/packages/app/src/cli/commands/app/import/channel-config.ts index 02780a5a284..f0602b2f0ba 100644 --- a/packages/app/src/cli/commands/app/import/channel-config.ts +++ b/packages/app/src/cli/commands/app/import/channel-config.ts @@ -3,6 +3,7 @@ import {linkedAppContext} from '../../../services/app-context.js' import {importChannelConfig} from '../../../services/import-channel-config/import.js' import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js' import {importChannelConfigJsonOutputSchema} from '../../../services/import-channel-config/types.js' +import {renderImportChannelConfigResult} from '../../../services/import-channel-config/result.js' import {Flags} from '@oclif/core' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' @@ -16,12 +17,12 @@ export default class ImportChannelConfig extends AppLinkedCommand { The generated TOML only includes public \`channel_config\` fields. Review it, then deploy it with \`shopify app deploy\`.` - static description = this.descriptionWithoutMarkdown() - static get jsonOutputSchema() { return importChannelConfigJsonOutputSchema } + static description = this.descriptionForHelp() + static flags = { ...globalFlags, ...appFlags, @@ -43,14 +44,19 @@ export default class ImportChannelConfig extends AppLinkedCommand { userProvidedConfigName: flags.config, }) - await importChannelConfig({ + const result = await importChannelConfig({ app, remoteApp, developerPlatformClient, force: flags.force, - json: flags.json, }) + renderImportChannelConfigResult( + result, + {directory: app.directory, name: remoteApp.title}, + flags.json ? 'json' : 'text', + ) + return {app} } } diff --git a/packages/app/src/cli/services/import-channel-config/import.test.ts b/packages/app/src/cli/services/import-channel-config/import.test.ts index 532428acfe4..50b0e48e181 100644 --- a/packages/app/src/cli/services/import-channel-config/import.test.ts +++ b/packages/app/src/cli/services/import-channel-config/import.test.ts @@ -8,17 +8,12 @@ import { testOrganizationApp, testUIExtension, } from '../../models/app/app.test-data.js' -import {afterEach, describe, expect, test, vi} from 'vitest' +import {describe, expect, test, vi} from 'vitest' import {fileExists, inTemporaryDirectory, mkdir, readFile, writeFile} from '@shopify/cli-kit/node/fs' import {dirname, joinPath} from '@shopify/cli-kit/node/path' -import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' vi.mock('./fetch.js') -afterEach(() => { - mockAndCaptureOutput().clear() -}) - const TOML = 'handle = "example"\nlabel = "Example Channel"\n' function successResult(warnings: {code: string; message: string}[] = []) { @@ -31,13 +26,12 @@ function successResult(warnings: {code: string; message: string}[] = []) { } } -function testOptions(app: AppLinkedInterface, {force = false, json = false} = {}) { +function testOptions(app: AppLinkedInterface, {force = false} = {}) { return { app, remoteApp: testOrganizationApp(), developerPlatformClient: testDeveloperPlatformClient(), force, - json, } } @@ -47,7 +41,6 @@ describe('importChannelConfig', () => { // Given vi.mocked(fetchChannelSpecExport).mockResolvedValue(successResult()) const app = testAppLinked({directory: tmpDir}) - const outputMock = mockAndCaptureOutput() // When await importChannelConfig(testOptions(app)) @@ -56,9 +49,6 @@ describe('importChannelConfig', () => { const outputPath = joinPath(tmpDir, CHANNEL_SPEC_DIRECTORY, 'example.toml') await expect(fileExists(outputPath)).resolves.toBe(true) await expect(readFile(outputPath)).resolves.toEqual(TOML) - expect(outputMock.info()).toContain('Imported the channel spec') - expect(outputMock.info()).toContain('shopify app dev') - expect(outputMock.info()).toContain('shopify app deploy') }) }) @@ -94,7 +84,24 @@ describe('importChannelConfig', () => { }) }) - test('renders backend warnings when writing the file', async () => { + test('validates the export before replacing an existing spec with --force', async () => { + await inTemporaryDirectory(async (tmpDir) => { + vi.mocked(fetchChannelSpecExport).mockResolvedValue({...successResult(), handle: ''}) + const app = testAppLinked({directory: tmpDir}) + const outputPath = joinPath(tmpDir, CHANNEL_SPEC_DIRECTORY, 'example.toml') + await mkdir(dirname(outputPath)) + await writeFile(outputPath, 'existing = true\n') + + await expect(importChannelConfig(testOptions(app, {force: true}))).rejects.toThrow() + + await expect(readFile(outputPath)).resolves.toEqual('existing = true\n') + await expect( + fileExists(joinPath(tmpDir, CHANNEL_SPEC_EXTENSION_DIRECTORY, 'shopify.extension.toml')), + ).resolves.toBe(false) + }) + }) + + test('returns backend warnings when writing the file', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given const warning = { @@ -104,13 +111,12 @@ describe('importChannelConfig', () => { } vi.mocked(fetchChannelSpecExport).mockResolvedValue(successResult([warning])) const app = testAppLinked({directory: tmpDir}) - const outputMock = mockAndCaptureOutput() // When - await importChannelConfig(testOptions(app)) + const result = await importChannelConfig(testOptions(app)) // Then - expect(outputMock.warn()).toContain('This generated spec enables automatic product feed management.') + expect(result.warnings).toEqual([warning]) await expect(readFile(joinPath(tmpDir, CHANNEL_SPEC_DIRECTORY, 'example.toml'))).resolves.not.toContain( 'product feed management', ) @@ -141,37 +147,11 @@ describe('importChannelConfig', () => { }) }) - test('emits the encoded JSON result and still writes the file in --json mode', async () => { - await inTemporaryDirectory(async (tmpDir) => { - // Given - const warning = {code: 'missing_countries', message: 'Add a countries section.'} - vi.mocked(fetchChannelSpecExport).mockResolvedValue(successResult([warning])) - const app = testAppLinked({directory: tmpDir}) - const outputMock = mockAndCaptureOutput() - - // When - await importChannelConfig(testOptions(app, {json: true})) - - // Then - const outputPath = joinPath(tmpDir, CHANNEL_SPEC_DIRECTORY, 'example.toml') - await expect(fileExists(outputPath)).resolves.toBe(true) - const parsed = JSON.parse(outputMock.info()) - expect(parsed).toEqual({ - handle: 'example', - filename: 'example.toml', - path: joinPath(CHANNEL_SPEC_DIRECTORY, 'example.toml'), - toml: TOML, - warnings: [warning], - }) - }) - }) - test('confines the write to the specifications directory when the filename contains path segments', async () => { await inTemporaryDirectory(async (tmpDir) => { // Given vi.mocked(fetchChannelSpecExport).mockResolvedValue({...successResult(), filename: '../../evil.toml'}) const app = testAppLinked({directory: tmpDir}) - mockAndCaptureOutput() // When await importChannelConfig(testOptions(app)) @@ -187,7 +167,6 @@ describe('importChannelConfig', () => { // Given vi.mocked(fetchChannelSpecExport).mockResolvedValue(successResult()) const app = testAppLinked({directory: tmpDir}) - const outputMock = mockAndCaptureOutput() // When await importChannelConfig(testOptions(app)) @@ -195,7 +174,6 @@ describe('importChannelConfig', () => { // Then const extensionConfigPath = joinPath(tmpDir, CHANNEL_SPEC_EXTENSION_DIRECTORY, 'shopify.extension.toml') await expect(readFile(extensionConfigPath)).resolves.toContain('type = "channel_config"') - expect(outputMock.info()).toContain('shopify.extension.toml') }) }) @@ -212,16 +190,15 @@ describe('importChannelConfig', () => { directory: tmpDir, allExtensions: [await testChannelConfigExtension(extensionDirectory, 'my-channel')], }) - const outputMock = mockAndCaptureOutput() // When - await importChannelConfig(testOptions(app)) + const result = await importChannelConfig(testOptions(app)) // Then await expect(readFile(joinPath(extensionDirectory, 'specifications', 'example.toml'))).resolves.toEqual(TOML) await expect(readFile(extensionConfigPath)).resolves.toEqual(existingContent) await expect(fileExists(joinPath(tmpDir, CHANNEL_SPEC_EXTENSION_DIRECTORY))).resolves.toBe(false) - expect(outputMock.info()).not.toContain('Also created') + expect(result.extensionConfigurationPath).toBeNull() }) }) diff --git a/packages/app/src/cli/services/import-channel-config/import.ts b/packages/app/src/cli/services/import-channel-config/import.ts index 1723917f559..090afc6a527 100644 --- a/packages/app/src/cli/services/import-channel-config/import.ts +++ b/packages/app/src/cli/services/import-channel-config/import.ts @@ -1,5 +1,5 @@ import {fetchChannelSpecExport} from './fetch.js' -import {importChannelConfigJsonOutputSchema} from './types.js' +import {importChannelConfigJsonOutputSchema, type ImportedChannelConfig} from './types.js' import {AppLinkedInterface} from '../../models/app/app.js' import { CHANNEL_CONFIG_IDENTIFIER, @@ -9,9 +9,7 @@ import {OrganizationApp} from '../../models/organization.js' import {DeveloperPlatformClient} from '../../utilities/developer-platform-client.js' import {AbortError} from '@shopify/cli-kit/node/error' import {fileExists, matchGlob, mkdir, writeFile} from '@shopify/cli-kit/node/fs' -import {basename, dirname, joinPath, relativePath} from '@shopify/cli-kit/node/path' -import {outputResult} from '@shopify/cli-kit/node/output' -import {renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' +import {basename, dirname, joinPath, relativePath, resolvePath} from '@shopify/cli-kit/node/path' export const CHANNEL_SPEC_EXTENSION_DIRECTORY = joinPath('extensions', 'channel-config') export const CHANNEL_SPEC_DIRECTORY = joinPath( @@ -36,15 +34,14 @@ interface ImportChannelConfigOptions { remoteApp: OrganizationApp developerPlatformClient: DeveloperPlatformClient force: boolean - json: boolean } /** * Imports the Shopify-authored channel spec for the linked app into the app's channel_config * extension, creating the extension if the app doesn't have one yet. */ -export async function importChannelConfig(options: ImportChannelConfigOptions): Promise { - const {app, remoteApp, developerPlatformClient, force, json} = options +export async function importChannelConfig(options: ImportChannelConfigOptions): Promise { + const {app, remoteApp, developerPlatformClient, force} = options const result = await fetchChannelSpecExport({remoteApp, developerPlatformClient}) @@ -57,7 +54,7 @@ export async function importChannelConfig(options: ImportChannelConfigOptions): const {extensionDirectory, createExtension} = resolveExtensionDirectory(app) // basename() so a filename with path separators can't escape the specifications directory - const outputPath = joinPath(extensionDirectory, CHANNEL_CONFIG_SPECIFICATIONS_DIRECTORY, basename(result.filename)) + const outputPath = resolvePath(extensionDirectory, CHANNEL_CONFIG_SPECIFICATIONS_DIRECTORY, basename(result.filename)) if (!force && (await fileExists(outputPath))) { throw new AbortError( `A channel spec already exists at ${relativePath(app.directory, outputPath)}.`, @@ -65,47 +62,25 @@ export async function importChannelConfig(options: ImportChannelConfigOptions): ) } + const imported = importChannelConfigJsonOutputSchema.validate({ + status: 'success', + handle: result.handle, + filename: basename(result.filename), + path: outputPath, + toml: result.toml, + warnings: result.warnings.map(({code, message}) => ({code, message})), + }) + await mkdir(dirname(outputPath)) if (createExtension) { await writeFile(joinPath(extensionDirectory, EXTENSION_CONFIG_FILENAME), EXTENSION_CONFIG_CONTENT) } - await writeFile(outputPath, result.toml) - - if (json) { - outputResult( - importChannelConfigJsonOutputSchema.encode({ - handle: result.handle, - filename: basename(result.filename), - path: relativePath(app.directory, outputPath), - toml: result.toml, - warnings: result.warnings, - }), - ) - return - } + await writeFile(outputPath, imported.toml) - result.warnings.forEach((warning) => renderWarning({body: warning.message})) - - renderSuccess({ - headline: ['Imported the channel spec for', {userInput: remoteApp.title}, {char: '.'}], - body: [ - 'The spec was written to', - {filePath: relativePath(app.directory, outputPath)}, - {char: '.'}, - ...(createExtension - ? [ - 'Also created', - {filePath: relativePath(app.directory, joinPath(extensionDirectory, EXTENSION_CONFIG_FILENAME))}, - 'so the spec is included when your app is deployed.', - ] - : []), - ], - nextSteps: [ - 'Review the generated spec and make any changes your channel needs.', - ['Run', {command: 'shopify app dev'}, 'to try the spec on a development store before releasing it.'], - ['Run', {command: 'shopify app deploy'}, 'to deploy the spec as part of your app.'], - ], - }) + return { + ...imported, + extensionConfigurationPath: createExtension ? resolvePath(extensionDirectory, EXTENSION_CONFIG_FILENAME) : null, + } } /** diff --git a/packages/app/src/cli/services/import-channel-config/result.test.ts b/packages/app/src/cli/services/import-channel-config/result.test.ts new file mode 100644 index 00000000000..0ca46fff9c1 --- /dev/null +++ b/packages/app/src/cli/services/import-channel-config/result.test.ts @@ -0,0 +1,38 @@ +import {renderImportChannelConfigResult} from './result.js' +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' +import {unstyled} from '@shopify/cli-kit/node/output' +import {resolvePath, joinPath} from '@shopify/cli-kit/node/path' +import {expect, test} from 'vitest' + +const directory = resolvePath('example-app') +const result = { + status: 'success' as const, + handle: 'example', + filename: 'example.toml', + path: joinPath(directory, 'extensions/channel-config/specifications/example.toml'), + toml: 'handle = "example"\nlabel = "Café channel"\n', + warnings: [{code: 'missing_countries', message: 'Add a countries section.'}], + extensionConfigurationPath: joinPath(directory, 'extensions/channel-config/shopify.extension.toml'), +} +const app = {directory, name: 'Example app'} + +test('keeps the warning and success text on stderr with relative file paths', async () => { + await withCapturedStandardStreams(async ({stdout, stderr}) => { + renderImportChannelConfigResult(result, app, 'text') + expect(stdout()).toBe('') + const text = unstyled(stderr()) + expect(text).toContain('Add a countries section.') + expect(text).toContain('Imported the channel spec for Example app.') + expect(text).toContain('Also created') + expect(text).toContain('extensions/channel-config/shopify.extension.toml') + expect(text).toContain('shopify app dev') + expect(text).toContain('shopify app deploy') + }) +}) + +test('omits the created-extension text when the extension already exists', async () => { + await withCapturedStandardStreams(async ({stderr}) => { + renderImportChannelConfigResult({...result, extensionConfigurationPath: null}, app, 'text') + expect(unstyled(stderr())).not.toContain('Also created') + }) +}) diff --git a/packages/app/src/cli/services/import-channel-config/result.ts b/packages/app/src/cli/services/import-channel-config/result.ts new file mode 100644 index 00000000000..ba2acfa2a05 --- /dev/null +++ b/packages/app/src/cli/services/import-channel-config/result.ts @@ -0,0 +1,46 @@ +import {importChannelConfigJsonOutputSchema, type ImportedChannelConfig} from './types.js' +import {relativePath} from '@shopify/cli-kit/node/path' +import {outputResult} from '@shopify/cli-kit/node/output' +import {renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui' + +export function renderImportChannelConfigResult( + result: ImportedChannelConfig, + app: {directory: string; name: string}, + format: 'json' | 'text', +): void { + result.warnings.forEach((warning) => renderWarning({body: warning.message})) + if (format === 'json') { + outputResult( + importChannelConfigJsonOutputSchema.encode({ + status: result.status, + handle: result.handle, + filename: result.filename, + path: result.path, + toml: result.toml, + warnings: result.warnings.map(({code, message}) => ({code, message})), + }), + ) + return + } + + renderSuccess({ + headline: ['Imported the channel spec for', {userInput: app.name}, {char: '.'}], + body: [ + 'The spec was written to', + {filePath: relativePath(app.directory, result.path)}, + {char: '.'}, + ...(result.extensionConfigurationPath + ? [ + 'Also created', + {filePath: relativePath(app.directory, result.extensionConfigurationPath)}, + 'so the spec is included when your app is deployed.', + ] + : []), + ], + nextSteps: [ + 'Review the generated spec and make any changes your channel needs.', + ['Run', {command: 'shopify app dev'}, 'to try the spec on a development store before releasing it.'], + ['Run', {command: 'shopify app deploy'}, 'to deploy the spec as part of your app.'], + ], + }) +} diff --git a/packages/app/src/cli/services/import-channel-config/types.ts b/packages/app/src/cli/services/import-channel-config/types.ts index c03efda7bfb..ab99cbdf651 100644 --- a/packages/app/src/cli/services/import-channel-config/types.ts +++ b/packages/app/src/cli/services/import-channel-config/types.ts @@ -1,19 +1,31 @@ -import {defineJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' import {zod} from '@shopify/cli-kit/node/schema' -const channelSpecExportWarningSchema = zod.object({ - code: zod.string(), - message: zod.string(), -}) +const channelSpecExportWarningSchema = zod + .object({ + code: zod.string(), + message: zod.string(), + }) + .strict() export const importChannelConfigJsonOutputSchema = defineJsonOutputSchema({ name: 'ImportChannelConfigResult', - schema: zod.object({ - handle: zod.string(), - filename: zod.string(), - path: zod.string(), - toml: zod.string(), - warnings: zod.array(channelSpecExportWarningSchema), - }), + schema: zod + .object({ + status: zod.literal('success'), + handle: zod.string().min(1), + filename: zod.string().min(1), + path: zod + .string() + .regex(/^(?:\/|[a-zA-Z]:[\\/]|\\\\)/, 'The path must be absolute.') + .describe('Absolute path of the written TOML file.'), + toml: zod.string().describe('Native channel_config TOML content, unchanged from the export.'), + warnings: zod.array(channelSpecExportWarningSchema).describe('Warnings returned with the channel spec export.'), + }) + .strict(), definitions: {ChannelSpecExportWarning: channelSpecExportWarningSchema}, }) + +export type ImportChannelConfigResult = InferJsonOutputSchema + +export type ImportedChannelConfig = ImportChannelConfigResult & {extensionConfigurationPath: string | null} diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 31e82b9ad49..dedabb291b0 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -3045,7 +3045,7 @@ "args": { }, "customPluginName": "@shopify/app", - "description": "Imports the Shopify-authored channel spec for your app as a `channel_config` extension.\n\n The generated TOML only includes public `channel_config` fields. Review it, then deploy it with `shopify app deploy`.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ImportChannelConfigResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"handle\": {\n \"type\": \"string\"\n },\n \"filename\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"toml\": {\n \"type\": \"string\"\n },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ChannelSpecExportWarning\"\n }\n }\n },\n \"required\": [\n \"handle\",\n \"filename\",\n \"path\",\n \"toml\",\n \"warnings\"\n ],\n \"additionalProperties\": false,\n \"title\": \"ImportChannelConfigResult\",\n \"definitions\": {\n \"ChannelSpecExportWarning\": {\n \"type\": \"object\",\n \"properties\": {\n \"code\": {\n \"type\": \"string\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"code\",\n \"message\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", + "description": "Imports the Shopify-authored channel spec for your app as a `channel_config` extension.\n\n The generated TOML only includes public `channel_config` fields. Review it, then deploy it with `shopify app deploy`.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ImportChannelConfigResult` schema.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"handle\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"filename\": {\n \"type\": \"string\",\n \"minLength\": 1\n },\n \"path\": {\n \"type\": \"string\",\n \"pattern\": \"^(?:\\\\/|[a-zA-Z]:[\\\\\\\\/]|\\\\\\\\\\\\\\\\)\",\n \"description\": \"Absolute path of the written TOML file.\"\n },\n \"toml\": {\n \"type\": \"string\",\n \"description\": \"Native channel_config TOML content, unchanged from the export.\"\n },\n \"warnings\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ChannelSpecExportWarning\"\n },\n \"description\": \"Warnings returned with the channel spec export.\"\n }\n },\n \"required\": [\n \"status\",\n \"handle\",\n \"filename\",\n \"path\",\n \"toml\",\n \"warnings\"\n ],\n \"additionalProperties\": false,\n \"title\": \"ImportChannelConfigResult\",\n \"definitions\": {\n \"ChannelSpecExportWarning\": {\n \"type\": \"object\",\n \"properties\": {\n \"code\": {\n \"type\": \"string\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"code\",\n \"message\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "descriptionWithMarkdown": "Imports the Shopify-authored channel spec for your app as a `channel_config` extension.\n\n The generated TOML only includes public `channel_config` fields. Review it, then deploy it with `shopify app deploy`.", "flags": { "auth-alias": {