Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework app info to use components #5170

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 5 additions & 0 deletions .changeset/light-windows-sit.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/app': minor
---

Give `app info` a facelift and correct a few display bugs
5 changes: 5 additions & 0 deletions .changeset/red-brooms-lick.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-kit': minor
---

Add tabular data display component to UI kit
20 changes: 12 additions & 8 deletions packages/app/src/cli/commands/app/info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {linkedAppContext} from '../../services/app-context.js'
import {Flags} from '@oclif/core'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
import {outputInfo} from '@shopify/cli-kit/node/output'
import {renderInfo} from '@shopify/cli-kit/node/ui'

export default class AppInfo extends AppCommand {
static summary = 'Print basic information about your app and extensions.'
Expand Down Expand Up @@ -40,14 +41,17 @@ export default class AppInfo extends AppCommand {
userProvidedConfigName: flags.config,
unsafeReportMode: true,
})
outputInfo(
await info(app, remoteApp, {
format: (flags.json ? 'json' : 'text') as Format,
webEnv: flags['web-env'],
configName: flags.config,
developerPlatformClient,
}),
)
const results = await info(app, remoteApp, {
format: (flags.json ? 'json' : 'text') as Format,
webEnv: flags['web-env'],
configName: flags.config,
developerPlatformClient,
})
if (typeof results === 'string' || 'value' in results) {
outputInfo(results)
} else {
renderInfo({customSections: results})
}
if (app.errors) process.exit(2)

return {app}
Expand Down
93 changes: 49 additions & 44 deletions packages/app/src/cli/services/info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,12 @@ import {
import {AppErrors} from '../models/app/loader.js'
import {DeveloperPlatformClient} from '../utilities/developer-platform-client.js'
import {describe, expect, vi, test} from 'vitest'
import {checkForNewVersion} from '@shopify/cli-kit/node/node-package-manager'
import {joinPath} from '@shopify/cli-kit/node/path'
import {TokenizedString, stringifyMessage, unstyled} from '@shopify/cli-kit/node/output'
import {OutputMessage, TokenizedString, stringifyMessage, unstyled} from '@shopify/cli-kit/node/output'
import {inTemporaryDirectory, writeFileSync} from '@shopify/cli-kit/node/fs'
import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version'
import {InlineToken, renderInfo} from '@shopify/cli-kit/node/ui'

type CustomSection = Exclude<Parameters<typeof renderInfo>[0]['customSections'], undefined>[number]

vi.mock('../prompts/dev.js')
vi.mock('@shopify/cli-kit/node/node-package-manager')
Expand Down Expand Up @@ -80,34 +81,6 @@ function infoOptions(): InfoOptions {
describe('info', () => {
const remoteApp = testOrganizationApp()

test('returns update shopify cli reminder when last version is greater than current version', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
const latestVersion = '2.2.3'
const app = mockApp({directory: tmp})
vi.mocked(checkForNewVersion).mockResolvedValue(latestVersion)

// When
const result = stringifyMessage(await info(app, remoteApp, infoOptions()))
// Then
expect(unstyled(result)).toMatch(`Shopify CLI ${CLI_KIT_VERSION}`)
})
})

test('returns update shopify cli reminder when last version lower or equals to current version', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
const app = mockApp({directory: tmp})
vi.mocked(checkForNewVersion).mockResolvedValue(undefined)

// When
const result = stringifyMessage(await info(app, remoteApp, infoOptions()))
// Then
expect(unstyled(result)).toMatch(`Shopify CLI ${CLI_KIT_VERSION}`)
expect(unstyled(result)).not.toMatch('CLI reminder')
})
})

test('returns the web environment as a text when webEnv is true', async () => {
await inTemporaryDirectory(async (tmp) => {
// Given
Expand All @@ -116,7 +89,7 @@ describe('info', () => {
vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1)

// When
const result = await info(app, remoteApp, {...infoOptions(), webEnv: true})
const result = (await info(app, remoteApp, {...infoOptions(), webEnv: true})) as OutputMessage

// Then
expect(unstyled(stringifyMessage(result))).toMatchInlineSnapshot(`
Expand All @@ -136,7 +109,7 @@ describe('info', () => {
vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1)

// When
const result = await info(app, remoteApp, {...infoOptions(), format: 'json', webEnv: true})
const result = (await info(app, remoteApp, {...infoOptions(), format: 'json', webEnv: true})) as OutputMessage

// Then
expect(unstyled(stringifyMessage(result))).toMatchInlineSnapshot(`
Expand Down Expand Up @@ -184,18 +157,28 @@ describe('info', () => {
vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1)

// When
const result = await info(app, remoteApp, infoOptions())
const result = (await info(app, remoteApp, infoOptions())) as CustomSection[]
const uiData = tabularDataSectionFromInfo(result, 'ui_extension_external')
const checkoutData = tabularDataSectionFromInfo(result, 'checkout_ui_extension_external')

// Then
expect(result).toContain('Extensions with errors')

// Doesn't use the type as part of the title
expect(result).not.toContain('📂 ui_extension')
// Shows handle in title
expect(result).toContain('📂 handle-for-extension-1')
expect(JSON.stringify(uiData)).not.toContain('📂 ui_extension')

// Shows handle as title
const uiExtensionTitle = uiData[0]![0]
expect(uiExtensionTitle).toBe('📂 handle-for-extension-1')
// Displays errors
const uiExtensionErrorsRow = errorRow(uiData)
expect(uiExtensionErrorsRow[1]).toStrictEqual({error: 'Mock error with ui_extension'})

// Shows default handle derived from name when no handle is present
expect(result).toContain('📂 extension-2')
expect(result).toContain('! Mock error with ui_extension')
expect(result).toContain('! Mock error with checkout_ui_extension')
const checkoutExtensionTitle = checkoutData[0]![0]
expect(checkoutExtensionTitle).toBe('📂 extension-2')
// Displays errors
const checkoutExtensionErrorsRow = errorRow(checkoutData)
expect(checkoutExtensionErrorsRow[1]).toStrictEqual({error: 'Mock error with checkout_ui_extension'})
})
})

Expand All @@ -222,11 +205,14 @@ describe('info', () => {
vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1)

// When
const result = await info(app, remoteApp, infoOptions())
const result = (await info(app, remoteApp, infoOptions())) as CustomSection[]
const uiExtensionsData = tabularDataSectionFromInfo(result, 'ui_extension_external')
const relevantExtension = extensionTitleRow(uiExtensionsData, 'handle-for-extension-1')
const irrelevantExtension = extensionTitleRow(uiExtensionsData, 'point_of_sale')

// Then
expect(result).toContain('📂 handle-for-extension-1')
expect(result).not.toContain('📂 point_of_sale')
expect(relevantExtension).toBeDefined()
expect(irrelevantExtension).not.toBeDefined()
})
})

Expand Down Expand Up @@ -293,3 +279,22 @@ function mockApp({
...(app ? app : {}),
})
}

function tabularDataSectionFromInfo(info: CustomSection[], title: string): InlineToken[][] {
const section = info.find((section) => section.title === title)
if (!section) throw new Error(`Section ${title} not found`)
if (!(typeof section.body === 'object' && 'tabularData' in section.body)) {
throw new Error(`Expected to be a table: ${JSON.stringify(section.body)}`)
}
return section.body.tabularData
}

function errorRow(data: InlineToken[][]): InlineToken[] {
const row = data.find((row: InlineToken[]) => typeof row[0] === 'object' && 'error' in row[0])!
if (!row) throw new Error('Error row not found')
return row
}

function extensionTitleRow(data: InlineToken[][], title: string): InlineToken[] | undefined {
return data.find((row) => typeof row[0] === 'string' && row[0].match(new RegExp(title)))
}
Loading
Loading