From b02d1f975f6ab90c64865027736c627b715adeb4 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 8 Jan 2025 13:27:55 +0200 Subject: [PATCH 01/13] Rework app info to use components --- packages/app/src/cli/commands/app/info.ts | 20 +- packages/app/src/cli/services/info.ts | 207 ++++++++++-------- .../src/private/node/ui/components/Alert.tsx | 9 +- .../private/node/ui/components/FatalError.tsx | 7 +- .../node/ui/components/TabularData.tsx | 36 +++ 5 files changed, 180 insertions(+), 99 deletions(-) create mode 100644 packages/cli-kit/src/private/node/ui/components/TabularData.tsx diff --git a/packages/app/src/cli/commands/app/info.ts b/packages/app/src/cli/commands/app/info.ts index c94727d0abc..be003403689 100644 --- a/packages/app/src/cli/commands/app/info.ts +++ b/packages/app/src/cli/commands/app/info.ts @@ -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.' @@ -40,14 +41,17 @@ export default class AppInfo extends AppCommand { userProvidedConfigName: flags.config, unsafeReportMode: true, }) - outputInfo( - await info(app, remoteApp, organization, { - format: (flags.json ? 'json' : 'text') as Format, - webEnv: flags['web-env'], - configName: flags.config, - developerPlatformClient, - }), - ) + const results = await info(app, remoteApp, organization, { + 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} diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 3adf9ea510e..7d2835354bd 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -6,11 +6,19 @@ import {configurationFileNames} from '../constants.js' import {ExtensionInstance} from '../models/extensions/extension-instance.js' import {Organization, OrganizationApp} from '../models/organization.js' import {platformAndArch} from '@shopify/cli-kit/node/os' -import {linesToColumns} from '@shopify/cli-kit/common/string' import {basename, relativePath} from '@shopify/cli-kit/node/path' -import {OutputMessage, outputContent, outputToken, formatSection, stringifyMessage} from '@shopify/cli-kit/node/output' +import { + OutputMessage, + formatPackageManagerCommand, + outputContent, + outputToken, + stringifyMessage, +} from '@shopify/cli-kit/node/output' +import {InlineToken, renderInfo} from '@shopify/cli-kit/node/ui' import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version' +type CustomSection = Exclude[0]['customSections'], undefined>[number] + export type Format = 'json' | 'text' export interface InfoOptions { format: Format @@ -19,17 +27,13 @@ export interface InfoOptions { webEnv: boolean developerPlatformClient: DeveloperPlatformClient } -interface Configurable { - type: string - externalType: string -} export async function info( app: AppLinkedInterface, remoteApp: OrganizationApp, organization: Organization, options: InfoOptions, -): Promise { +): Promise { if (options.webEnv) { return infoWeb(app, remoteApp, organization, options) } else { @@ -50,7 +54,7 @@ async function infoApp( app: AppLinkedInterface, remoteApp: OrganizationApp, options: InfoOptions, -): Promise { +): Promise { if (options.format === 'json') { const extensionsInfo = withPurgedSchemas(app.allExtensions.filter((ext) => ext.isReturnedAsInfo())) let appWithSupportedExtensions = { @@ -119,30 +123,22 @@ class AppInfo { this.options = options } - async output(): Promise { - const sections: [string, string][] = [ - await this.devConfigsSection(), + async output(): Promise { + return [ + ...(await this.devConfigsSection()), this.projectSettingsSection(), - await this.appComponentsSection(), + ...(await this.appComponentsSection()), await this.systemInfoSection(), ] - return sections.map((sectionContents: [string, string]) => formatSection(...sectionContents)).join('\n\n') } - async devConfigsSection(): Promise<[string, string]> { - const title = `Current app configuration` - const postscript = outputContent`💡 To change these, run ${outputToken.packagejsonScript( - this.app.packageManager, - 'dev', - '--reset', - )}`.value - + async devConfigsSection(): Promise { let updateUrls = NOT_CONFIGURED_TEXT if (this.app.configuration.build?.automatically_update_urls_on_dev !== undefined) { updateUrls = this.app.configuration.build.automatically_update_urls_on_dev ? 'Yes' : 'No' } - let partnersAccountInfo = ['Partners account', 'unknown'] + let partnersAccountInfo: [string, string] = ['Partners account', 'unknown'] const retrievedAccountInfo = await this.options.developerPlatformClient.accountInfo() if (isServiceAccount(retrievedAccountInfo)) { partnersAccountInfo = ['Service account', retrievedAccountInfo.orgName] @@ -150,108 +146,131 @@ class AppInfo { partnersAccountInfo = ['Partners account', retrievedAccountInfo.email] } - const lines = [ - ['Configuration file', basename(this.app.configuration.path) || configurationFileNames.app], - ['App name', this.remoteApp.title || NOT_CONFIGURED_TEXT], - ['Client ID', this.remoteApp.apiKey || NOT_CONFIGURED_TEXT], - ['Access scopes', getAppScopes(this.app.configuration)], - ['Dev store', this.app.configuration.build?.dev_store_url || NOT_CONFIGURED_TEXT], - ['Update URLs', updateUrls], - partnersAccountInfo, + return [ + this.tableSection( + 'Current app configuration', + [ + ['Configuration file', {filePath: basename(this.app.configuration.path) || configurationFileNames.app}], + ['App name', this.remoteApp.title || NOT_CONFIGURED_TEXT], + ['Client ID', this.remoteApp.apiKey || NOT_CONFIGURED_TEXT], + ['Access scopes', getAppScopes(this.app.configuration)], + ['Dev store', this.app.configuration.build?.dev_store_url || NOT_CONFIGURED_TEXT], + ['Update URLs', updateUrls], + partnersAccountInfo, + ], + {isFirstItem: true}, + ), + { + body: [ + '💡 To change these, run', + {command: formatPackageManagerCommand(this.app.packageManager, 'dev', '--reset')}, + ], + }, ] - return [title, `${linesToColumns(lines)}\n\n${postscript}`] } - projectSettingsSection(): [string, string] { - const title = 'Your Project' - const lines = [['Root location', this.app.directory]] - return [title, linesToColumns(lines)] + projectSettingsSection(): CustomSection { + return this.tableSection('Your Project', [['Root location', {filePath: this.app.directory}]]) } - async appComponentsSection(): Promise<[string, string]> { - const title = 'Directory Components' - - let body = this.webComponentsSection() - - function augmentWithExtensions( - extensions: TExtension[], - outputFormatter: (extension: TExtension) => string, - ) { - const types = new Set(extensions.map((ext) => ext.type)) - types.forEach((extensionType: string) => { - const relevantExtensions = extensions.filter((extension: TExtension) => extension.type === extensionType) - if (relevantExtensions[0]) { - body += `\n\n${outputContent`${outputToken.subheading(relevantExtensions[0].externalType)}`.value}` - relevantExtensions.forEach((extension: TExtension) => { - body += outputFormatter(extension) - }) - } - }) - } + async appComponentsSection(): Promise { + const webComponentsSection = this.webComponentsSection() const supportedExtensions = this.app.allExtensions.filter((ext) => ext.isReturnedAsInfo()) - augmentWithExtensions(supportedExtensions, this.extensionSubSection.bind(this)) + const extensionsSections = this.extensionsSections(supportedExtensions) + let errorsSection: CustomSection | undefined if (this.app.errors?.isEmpty() === false) { - body += `\n\n${outputContent`${outputToken.subheading('Extensions with errors')}`.value}` - supportedExtensions.forEach((extension) => { - body += this.invalidExtensionSubSection(extension) - }) + errorsSection = this.tableSection( + 'Extensions with errors', + ( + supportedExtensions + .map((extension) => this.invalidExtensionSubSection(extension)) + .filter((data) => typeof data !== 'undefined') as [string, InlineToken][][] + ).flat(), + ) } - return [title, body] + + return [ + { + title: '\nDirectory components'.toUpperCase(), + body: '', + }, + ...(webComponentsSection ? [webComponentsSection] : []), + ...extensionsSections, + ...(errorsSection ? [errorsSection] : []), + ] } - webComponentsSection(): string { + webComponentsSection(): CustomSection | undefined { const errors: OutputMessage[] = [] - const subtitle = outputContent`${outputToken.subheading('web')}`.value - const toplevel = ['📂 web', ''] - const sublevels: [string, string][] = [] + const sublevels: [string, InlineToken][] = [] + if (!this.app.webs[0]) return this.app.webs.forEach((web) => { if (web.configuration) { if (web.configuration.name) { const {name, roles} = web.configuration - sublevels.push([` 📂 ${name} (${roles.join(',')})`, relativePath(this.app.directory, web.directory)]) + sublevels.push([ + ` 📂 ${name} (${roles.join(',')})`, + {filePath: relativePath(this.app.directory, web.directory)}, + ]) } else { web.configuration.roles.forEach((role) => { - sublevels.push([` 📂 ${role}`, relativePath(this.app.directory, web.directory)]) + sublevels.push([` 📂 ${role}`, {filePath: relativePath(this.app.directory, web.directory)}]) }) } } else { - sublevels.push([` 📂 ${UNKNOWN_TEXT}`, relativePath(this.app.directory, web.directory)]) + sublevels.push([` 📂 ${UNKNOWN_TEXT}`, {filePath: relativePath(this.app.directory, web.directory)}]) } if (this.app.errors) { const error = this.app.errors.getError(`${web.directory}/${configurationFileNames.web}`) if (error) errors.push(error) } }) - let errorContent = `\n${errors.map((error) => this.formattedError(error)).join('\n')}` - if (errorContent.trim() === '') errorContent = '' - return `${subtitle}\n${linesToColumns([toplevel, ...sublevels])}${errorContent}` + return this.subtableSection('web', [ + ['📂 web', ''], + ...sublevels, + ...errors.map((error): [string, InlineToken] => ['', {error: this.formattedError(error)}]), + ]) } - extensionSubSection(extension: ExtensionInstance): string { + extensionsSections(extensions: ExtensionInstance[]): CustomSection[] { + const types = Array.from(new Set(extensions.map((ext) => ext.type))) + return types + .map((extensionType: string): CustomSection | undefined => { + const relevantExtensions = extensions.filter((extension: ExtensionInstance) => extension.type === extensionType) + if (relevantExtensions[0]) { + return this.subtableSection( + relevantExtensions[0].externalType, + relevantExtensions.map((ext) => this.extensionSubSection(ext)).flat(), + ) + } + }) + .filter((section: CustomSection | undefined) => section !== undefined) as CustomSection[] + } + + extensionSubSection(extension: ExtensionInstance): [string, InlineToken][] { const config = extension.configuration - const details = [ - [`📂 ${extension.handle}`, relativePath(this.app.directory, extension.directory)], - [' config file', relativePath(extension.directory, extension.configurationPath)], + const details: [string, InlineToken][] = [ + [`📂 ${extension.handle}`, {filePath: relativePath(this.app.directory, extension.directory)}], + [' config file', {filePath: relativePath(extension.directory, extension.configurationPath)}], ] if (config && config.metafields?.length) { details.push([' metafields', `${config.metafields.length}`]) } - return `\n${linesToColumns(details)}` + return details } - invalidExtensionSubSection(extension: ExtensionInstance): string { + invalidExtensionSubSection(extension: ExtensionInstance): [string, InlineToken][] | undefined { const error = this.app.errors?.getError(extension.configurationPath) - if (!error) return '' - const details = [ - [`📂 ${extension.handle}`, relativePath(this.app.directory, extension.directory)], - [' config file', relativePath(extension.directory, extension.configurationPath)], + if (!error) return + return [ + [`📂 ${extension.handle}`, {filePath: relativePath(this.app.directory, extension.directory)}], + [' config file', {filePath: relativePath(extension.directory, extension.configurationPath)}], + [' message', {error: this.formattedError(error)}], ] - const formattedError = this.formattedError(error) - return `\n${linesToColumns(details)}\n${formattedError}` } formattedError(str: OutputMessage): string { @@ -260,16 +279,28 @@ class AppInfo { return outputContent`${outputToken.errorText(errorLines.join('\n'))}`.value } - async systemInfoSection(): Promise<[string, string]> { - const title = 'Tooling and System' + async systemInfoSection(): Promise { const {platform, arch} = platformAndArch() - const lines: string[][] = [ + return this.tableSection('Tooling and System', [ ['Shopify CLI', CLI_KIT_VERSION], ['Package manager', this.app.packageManager], ['OS', `${platform}-${arch}`], ['Shell', process.env.SHELL || 'unknown'], ['Node version', process.version], - ] - return [title, linesToColumns(lines)] + ]) + } + + tableSection(title: string, rows: [string, InlineToken][], {isFirstItem = false} = {}): CustomSection { + return { + title: `${isFirstItem ? '' : '\n'}${title.toUpperCase()}\n`, + body: {tabularData: rows, firstColumnSubdued: true}, + } + } + + subtableSection(title: string, rows: [string, InlineToken][]): CustomSection { + return { + title, + body: {tabularData: rows, firstColumnSubdued: true}, + } } } diff --git a/packages/cli-kit/src/private/node/ui/components/Alert.tsx b/packages/cli-kit/src/private/node/ui/components/Alert.tsx index f732f33b833..fc573cd55dc 100644 --- a/packages/cli-kit/src/private/node/ui/components/Alert.tsx +++ b/packages/cli-kit/src/private/node/ui/components/Alert.tsx @@ -2,12 +2,13 @@ import {Banner, BannerType} from './Banner.js' import {Link} from './Link.js' import {List} from './List.js' import {BoldToken, InlineToken, LinkToken, TokenItem, TokenizedText} from './TokenizedText.js' +import {TabularData, TabularDataProps} from './TabularData.js' import {Box, Text} from 'ink' import React, {FunctionComponent} from 'react' export interface CustomSection { title?: string - body: TokenItem + body: TabularDataProps | TokenItem } export interface AlertProps { @@ -57,7 +58,11 @@ const Alert: FunctionComponent = ({ {customSections.map((section, index) => ( {section.title ? {section.title} : null} - + {typeof section.body === 'object' && 'tabularData' in section.body ? ( + + ) : ( + + )} ))} diff --git a/packages/cli-kit/src/private/node/ui/components/FatalError.tsx b/packages/cli-kit/src/private/node/ui/components/FatalError.tsx index c5e65dafcbe..b632ac0d177 100644 --- a/packages/cli-kit/src/private/node/ui/components/FatalError.tsx +++ b/packages/cli-kit/src/private/node/ui/components/FatalError.tsx @@ -2,6 +2,7 @@ import {Banner} from './Banner.js' import {TokenizedText} from './TokenizedText.js' import {Command} from './Command.js' import {List} from './List.js' +import {TabularData} from './TabularData.js' import {BugError, cleanSingleStackTracePath, ExternalError, FatalError as Fatal} from '../../../../public/node/error.js' import {Box, Text} from 'ink' import React, {FunctionComponent} from 'react' @@ -58,7 +59,11 @@ const FatalError: FunctionComponent = ({error}) => { {error.customSections.map((section, index) => ( {section.title ? {section.title} : null} - + {typeof section.body === 'object' && 'tabularData' in section.body ? ( + + ) : ( + + )} ))} diff --git a/packages/cli-kit/src/private/node/ui/components/TabularData.tsx b/packages/cli-kit/src/private/node/ui/components/TabularData.tsx new file mode 100644 index 00000000000..d97d238c219 --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/components/TabularData.tsx @@ -0,0 +1,36 @@ +import {InlineToken, TokenizedText, tokenItemToString} from './TokenizedText.js' +import {unstyled} from '../../../../public/node/output.js' +import {Box} from 'ink' +import React, {FunctionComponent} from 'react' + +export interface TabularDataProps { + tabularData: InlineToken[][] + firstColumnSubdued?: boolean +} + +const TabularData: FunctionComponent = ({tabularData: data, firstColumnSubdued}) => { + const columnWidths: number[] = data.reduce((acc, row) => { + row.forEach((cell, index) => { + acc[index] = Math.max(acc[index] ?? 0, unstyled((tokenItemToString(cell))).length) + }) + return acc + }, []) + + return ( + + {data.map((row, index) => ( + + {row.map((cell, index) => ( + + + + ))} + + ))} + + ) +} + +export {TabularData} From 17d42a2b1dea6583de107d5fcd62dacafffcf49e Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Wed, 8 Jan 2025 14:54:45 +0200 Subject: [PATCH 02/13] Improve handling of error messages --- packages/app/src/cli/services/info.ts | 77 +++++++------------ .../node/ui/components/TabularData.tsx | 2 +- 2 files changed, 30 insertions(+), 49 deletions(-) diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 7d2835354bd..8a533c100ad 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -11,7 +11,7 @@ import { OutputMessage, formatPackageManagerCommand, outputContent, - outputToken, + shouldDisplayColors, stringifyMessage, } from '@shopify/cli-kit/node/output' import {InlineToken, renderInfo} from '@shopify/cli-kit/node/ui' @@ -109,8 +109,9 @@ function withPurgedSchemas(extensions: object[]): object[] { }) } -const UNKNOWN_TEXT = outputContent`${outputToken.italic('unknown')}`.value -const NOT_CONFIGURED_TEXT = outputContent`${outputToken.italic('Not yet configured')}`.value +const UNKNOWN_TEXT = 'unknown' +const NOT_CONFIGURED_TOKEN: InlineToken = {subdued: 'Not yet configured'} +const NOT_LOADED_TEXT = 'NOT LOADED' class AppInfo { private readonly app: AppLinkedInterface @@ -133,7 +134,7 @@ class AppInfo { } async devConfigsSection(): Promise { - let updateUrls = NOT_CONFIGURED_TEXT + let updateUrls = NOT_CONFIGURED_TOKEN if (this.app.configuration.build?.automatically_update_urls_on_dev !== undefined) { updateUrls = this.app.configuration.build.automatically_update_urls_on_dev ? 'Yes' : 'No' } @@ -151,10 +152,10 @@ class AppInfo { 'Current app configuration', [ ['Configuration file', {filePath: basename(this.app.configuration.path) || configurationFileNames.app}], - ['App name', this.remoteApp.title || NOT_CONFIGURED_TEXT], - ['Client ID', this.remoteApp.apiKey || NOT_CONFIGURED_TEXT], + ['App name', this.remoteApp.title || NOT_CONFIGURED_TOKEN], + ['Client ID', this.remoteApp.apiKey || NOT_CONFIGURED_TOKEN], ['Access scopes', getAppScopes(this.app.configuration)], - ['Dev store', this.app.configuration.build?.dev_store_url || NOT_CONFIGURED_TEXT], + ['Dev store', this.app.configuration.build?.dev_store_url ?? NOT_CONFIGURED_TOKEN], ['Update URLs', updateUrls], partnersAccountInfo, ], @@ -175,36 +176,19 @@ class AppInfo { async appComponentsSection(): Promise { const webComponentsSection = this.webComponentsSection() - - const supportedExtensions = this.app.allExtensions.filter((ext) => ext.isReturnedAsInfo()) - const extensionsSections = this.extensionsSections(supportedExtensions) - - let errorsSection: CustomSection | undefined - if (this.app.errors?.isEmpty() === false) { - errorsSection = this.tableSection( - 'Extensions with errors', - ( - supportedExtensions - .map((extension) => this.invalidExtensionSubSection(extension)) - .filter((data) => typeof data !== 'undefined') as [string, InlineToken][][] - ).flat(), - ) - } - return [ { title: '\nDirectory components'.toUpperCase(), body: '', }, ...(webComponentsSection ? [webComponentsSection] : []), - ...extensionsSections, - ...(errorsSection ? [errorsSection] : []), + ...this.extensionsSections(), ] } webComponentsSection(): CustomSection | undefined { const errors: OutputMessage[] = [] - const sublevels: [string, InlineToken][] = [] + const sublevels: InlineToken[][] = [] if (!this.app.webs[0]) return this.app.webs.forEach((web) => { if (web.configuration) { @@ -220,7 +204,7 @@ class AppInfo { }) } } else { - sublevels.push([` 📂 ${UNKNOWN_TEXT}`, {filePath: relativePath(this.app.directory, web.directory)}]) + sublevels.push([{subdued: ` 📂 ${UNKNOWN_TEXT}`}, {filePath: relativePath(this.app.directory, web.directory)}]) } if (this.app.errors) { const error = this.app.errors.getError(`${web.directory}/${configurationFileNames.web}`) @@ -231,11 +215,12 @@ class AppInfo { return this.subtableSection('web', [ ['📂 web', ''], ...sublevels, - ...errors.map((error): [string, InlineToken] => ['', {error: this.formattedError(error)}]), + ...errors.map((error): InlineToken[] => [{error: 'error'}, {error: this.formattedError(error)}]), ]) } - extensionsSections(extensions: ExtensionInstance[]): CustomSection[] { + extensionsSections(): CustomSection[] { + const extensions = this.app.allExtensions.filter((ext) => ext.isReturnedAsInfo()) const types = Array.from(new Set(extensions.map((ext) => ext.type))) return types .map((extensionType: string): CustomSection | undefined => { @@ -250,33 +235,29 @@ class AppInfo { .filter((section: CustomSection | undefined) => section !== undefined) as CustomSection[] } - extensionSubSection(extension: ExtensionInstance): [string, InlineToken][] { + extensionSubSection(extension: ExtensionInstance): InlineToken[][] { const config = extension.configuration - const details: [string, InlineToken][] = [ - [`📂 ${extension.handle}`, {filePath: relativePath(this.app.directory, extension.directory)}], + const details: InlineToken[][] = [ + [`📂 ${extension.handle || NOT_LOADED_TEXT}`, {filePath: relativePath(this.app.directory, extension.directory)}], [' config file', {filePath: relativePath(extension.directory, extension.configurationPath)}], ] if (config && config.metafields?.length) { details.push([' metafields', `${config.metafields.length}`]) } + const error = this.app.errors?.getError(extension.configurationPath) + if (error) { + details.push([{error: ' error'}, {error: this.formattedError(error)}]) + } return details } - invalidExtensionSubSection(extension: ExtensionInstance): [string, InlineToken][] | undefined { - const error = this.app.errors?.getError(extension.configurationPath) - if (!error) return - return [ - [`📂 ${extension.handle}`, {filePath: relativePath(this.app.directory, extension.directory)}], - [' config file', {filePath: relativePath(extension.directory, extension.configurationPath)}], - [' message', {error: this.formattedError(error)}], - ] - } - formattedError(str: OutputMessage): string { - const [errorFirstLine, ...errorRemainingLines] = stringifyMessage(str).split('\n') - const errorLines = [`! ${errorFirstLine}`, ...errorRemainingLines.map((line) => ` ${line}`)] - return outputContent`${outputToken.errorText(errorLines.join('\n'))}`.value + // Some errors have newlines at the beginning for no apparent reason + const rawErrorMessage = stringifyMessage(str).trim() + if (shouldDisplayColors()) return rawErrorMessage + const [errorFirstLine, ...errorRemainingLines] = stringifyMessage(str).trim().split('\n') + return [`! ${errorFirstLine}`, ...errorRemainingLines.map((line) => ` ${line}`)].join('\n') } async systemInfoSection(): Promise { @@ -285,19 +266,19 @@ class AppInfo { ['Shopify CLI', CLI_KIT_VERSION], ['Package manager', this.app.packageManager], ['OS', `${platform}-${arch}`], - ['Shell', process.env.SHELL || 'unknown'], + ['Shell', process.env.SHELL ?? 'unknown'], ['Node version', process.version], ]) } - tableSection(title: string, rows: [string, InlineToken][], {isFirstItem = false} = {}): CustomSection { + tableSection(title: string, rows: InlineToken[][], {isFirstItem = false} = {}): CustomSection { return { title: `${isFirstItem ? '' : '\n'}${title.toUpperCase()}\n`, body: {tabularData: rows, firstColumnSubdued: true}, } } - subtableSection(title: string, rows: [string, InlineToken][]): CustomSection { + subtableSection(title: string, rows: InlineToken[][]): CustomSection { return { title, body: {tabularData: rows, firstColumnSubdued: true}, diff --git a/packages/cli-kit/src/private/node/ui/components/TabularData.tsx b/packages/cli-kit/src/private/node/ui/components/TabularData.tsx index d97d238c219..b865da1dbfe 100644 --- a/packages/cli-kit/src/private/node/ui/components/TabularData.tsx +++ b/packages/cli-kit/src/private/node/ui/components/TabularData.tsx @@ -21,7 +21,7 @@ const TabularData: FunctionComponent = ({tabularData: data, fi {data.map((row, index) => ( {row.map((cell, index) => ( - + From 0db0229112a4f0b352bed788bcc6afb4215ef2d4 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 9 Jan 2025 13:07:12 +0200 Subject: [PATCH 03/13] Improve display of web section 1. Add a default relative path (otherwise relativePath returns a blank string) 2. Move roles to a separate line item --- packages/app/src/cli/services/info.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 8a533c100ad..6c2dcea9ccf 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -194,10 +194,11 @@ class AppInfo { if (web.configuration) { if (web.configuration.name) { const {name, roles} = web.configuration - sublevels.push([ - ` 📂 ${name} (${roles.join(',')})`, - {filePath: relativePath(this.app.directory, web.directory)}, - ]) + const pathToWeb = relativePath(this.app.directory, web.directory) + sublevels.push([` 📂 ${name}`, {filePath: pathToWeb || '/'}]) + if (roles.length > 0) { + sublevels.push([' roles', roles.join(', ')]) + } } else { web.configuration.roles.forEach((role) => { sublevels.push([` 📂 ${role}`, {filePath: relativePath(this.app.directory, web.directory)}]) From 2c643e3df1fe51bac7ed3778917d30f8d846c9ba Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 9 Jan 2025 14:08:08 +0200 Subject: [PATCH 04/13] Update tests to match new rendering system --- packages/app/src/cli/services/info.test.ts | 63 +++++++++++++++++----- 1 file changed, 49 insertions(+), 14 deletions(-) diff --git a/packages/app/src/cli/services/info.test.ts b/packages/app/src/cli/services/info.test.ts index 15c4161cf1d..4b86c0bb70a 100644 --- a/packages/app/src/cli/services/info.test.ts +++ b/packages/app/src/cli/services/info.test.ts @@ -14,10 +14,13 @@ 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 {InlineToken, renderInfo} from '@shopify/cli-kit/node/ui' import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version' +type CustomSection = Exclude[0]['customSections'], undefined>[number] + vi.mock('../prompts/dev.js') vi.mock('@shopify/cli-kit/node/node-package-manager') vi.mock('../utilities/developer-platform-client.js') @@ -116,7 +119,7 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = await info(app, remoteApp, ORG1, {...infoOptions(), webEnv: true}) + const result = (await info(app, remoteApp, ORG1, {...infoOptions(), webEnv: true})) as OutputMessage // Then expect(unstyled(stringifyMessage(result))).toMatchInlineSnapshot(` @@ -136,7 +139,7 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = await info(app, remoteApp, ORG1, {...infoOptions(), format: 'json', webEnv: true}) + const result = (await info(app, remoteApp, ORG1, {...infoOptions(), format: 'json', webEnv: true})) as OutputMessage // Then expect(unstyled(stringifyMessage(result))).toMatchInlineSnapshot(` @@ -184,18 +187,28 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = await info(app, remoteApp, ORG1, infoOptions()) + const result = await info(app, remoteApp, ORG1, 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'}) }) }) @@ -222,11 +235,14 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = await info(app, remoteApp, ORG1, infoOptions()) + const result = (await info(app, remoteApp, ORG1, 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() }) }) @@ -293,3 +309,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))) +} From 6ce820ad83fc4977f0fc973bbefcd749b7fec8b0 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 9 Jan 2025 14:08:40 +0200 Subject: [PATCH 05/13] Remove no-longer-relevant tests We removed the upgrade reminders in https://github.com/Shopify/cli/pull/4310 but we missed deleting the tests! --- packages/app/src/cli/services/info.test.ts | 30 ---------------------- 1 file changed, 30 deletions(-) diff --git a/packages/app/src/cli/services/info.test.ts b/packages/app/src/cli/services/info.test.ts index 4b86c0bb70a..dc1ac758df2 100644 --- a/packages/app/src/cli/services/info.test.ts +++ b/packages/app/src/cli/services/info.test.ts @@ -12,12 +12,10 @@ 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 {OutputMessage, TokenizedString, stringifyMessage, unstyled} from '@shopify/cli-kit/node/output' import {inTemporaryDirectory, writeFileSync} from '@shopify/cli-kit/node/fs' import {InlineToken, renderInfo} from '@shopify/cli-kit/node/ui' -import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version' type CustomSection = Exclude[0]['customSections'], undefined>[number] @@ -83,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, ORG1, 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, ORG1, 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 From 3ce2ba2f84ea36cc9dee2db7f43a6e84f12a785a Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 9 Jan 2025 16:33:38 +0200 Subject: [PATCH 06/13] Lint fixes --- packages/app/src/cli/services/info.ts | 2 +- packages/cli-kit/src/private/node/ui/components/TabularData.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 6c2dcea9ccf..494c7d3ff64 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -233,7 +233,7 @@ class AppInfo { ) } }) - .filter((section: CustomSection | undefined) => section !== undefined) as CustomSection[] + .filter((section: CustomSection | undefined) => section !== undefined) } extensionSubSection(extension: ExtensionInstance): InlineToken[][] { diff --git a/packages/cli-kit/src/private/node/ui/components/TabularData.tsx b/packages/cli-kit/src/private/node/ui/components/TabularData.tsx index b865da1dbfe..a228308af54 100644 --- a/packages/cli-kit/src/private/node/ui/components/TabularData.tsx +++ b/packages/cli-kit/src/private/node/ui/components/TabularData.tsx @@ -11,7 +11,7 @@ export interface TabularDataProps { const TabularData: FunctionComponent = ({tabularData: data, firstColumnSubdued}) => { const columnWidths: number[] = data.reduce((acc, row) => { row.forEach((cell, index) => { - acc[index] = Math.max(acc[index] ?? 0, unstyled((tokenItemToString(cell))).length) + acc[index] = Math.max(acc[index] ?? 0, unstyled(tokenItemToString(cell)).length) }) return acc }, []) From e8ee7548dad4e34f079145ac4eefdfa5456fa3bf Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 9 Jan 2025 17:33:51 +0200 Subject: [PATCH 07/13] Refer to User instead of Partners account because we may not be on Partners --- packages/app/src/cli/services/info.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 494c7d3ff64..42fcd83269b 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -139,12 +139,12 @@ class AppInfo { updateUrls = this.app.configuration.build.automatically_update_urls_on_dev ? 'Yes' : 'No' } - let partnersAccountInfo: [string, string] = ['Partners account', 'unknown'] + let userAccountInfo: [string, string] = ['User', 'unknown'] const retrievedAccountInfo = await this.options.developerPlatformClient.accountInfo() if (isServiceAccount(retrievedAccountInfo)) { - partnersAccountInfo = ['Service account', retrievedAccountInfo.orgName] + userAccountInfo = ['Service account', retrievedAccountInfo.orgName] } else if (isUserAccount(retrievedAccountInfo)) { - partnersAccountInfo = ['Partners account', retrievedAccountInfo.email] + userAccountInfo[1] = retrievedAccountInfo.email } return [ @@ -157,7 +157,7 @@ class AppInfo { ['Access scopes', getAppScopes(this.app.configuration)], ['Dev store', this.app.configuration.build?.dev_store_url ?? NOT_CONFIGURED_TOKEN], ['Update URLs', updateUrls], - partnersAccountInfo, + userAccountInfo, ], {isFirstItem: true}, ), From bb3b100731eb49c982ab60cd9b00890ffe33660f Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 9 Jan 2025 17:51:12 +0200 Subject: [PATCH 08/13] Add changesets --- .changeset/light-windows-sit.md | 5 +++++ .changeset/red-brooms-lick.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/light-windows-sit.md create mode 100644 .changeset/red-brooms-lick.md diff --git a/.changeset/light-windows-sit.md b/.changeset/light-windows-sit.md new file mode 100644 index 00000000000..07598bb80a8 --- /dev/null +++ b/.changeset/light-windows-sit.md @@ -0,0 +1,5 @@ +--- +'@shopify/app': minor +--- + +Give `app info` a facelift and correct a few display bugs diff --git a/.changeset/red-brooms-lick.md b/.changeset/red-brooms-lick.md new file mode 100644 index 00000000000..d4fdc36ea7a --- /dev/null +++ b/.changeset/red-brooms-lick.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli-kit': minor +--- + +Add tabular data display component to UI kit From b19fce5e725c955dc7904a38194631a371eaeb4f Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 9 Jan 2025 21:11:13 +0200 Subject: [PATCH 09/13] Render app name as userInput token --- packages/app/src/cli/services/info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 42fcd83269b..87856124279 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -152,7 +152,7 @@ class AppInfo { 'Current app configuration', [ ['Configuration file', {filePath: basename(this.app.configuration.path) || configurationFileNames.app}], - ['App name', this.remoteApp.title || NOT_CONFIGURED_TOKEN], + ['App name', this.remoteApp.title ? {userInput: this.remoteApp.title} : NOT_CONFIGURED_TOKEN], ['Client ID', this.remoteApp.apiKey || NOT_CONFIGURED_TOKEN], ['Access scopes', getAppScopes(this.app.configuration)], ['Dev store', this.app.configuration.build?.dev_store_url ?? NOT_CONFIGURED_TOKEN], From 261ee48f7598dea409b79b21a60f5691ce32a86a Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Thu, 9 Jan 2025 21:11:26 +0200 Subject: [PATCH 10/13] Add example of tabularData to kitchen sink --- .../cli/src/cli/services/kitchen-sink/static.ts | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/cli/src/cli/services/kitchen-sink/static.ts b/packages/cli/src/cli/services/kitchen-sink/static.ts index c6b342f8207..32a01e8c1e6 100644 --- a/packages/cli/src/cli/services/kitchen-sink/static.ts +++ b/packages/cli/src/cli/services/kitchen-sink/static.ts @@ -34,6 +34,22 @@ export async function staticService() { ], }) + renderInfo({ + headline: 'About your app', + customSections: [ + { + body: { + tabularData: [ + ['Configuration file', {filePath: 'shopify.app.scalable-transaction-app.toml'}], + ['App name', {userInput: 'scalable-transaction-app'}], + ['Access scopes', 'read_products,write_products'], + ], + firstColumnSubdued: true, + }, + }, + ], + }) + renderInfo({ headline: [{userInput: 'my-app'}, 'initialized and ready to build.'], nextSteps: [ From 67b19b75131897426c7cc0203bf22837714eb1f5 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Mon, 13 Jan 2025 15:04:19 +0200 Subject: [PATCH 11/13] Use exported AlertCustomSection type --- packages/app/src/cli/services/info.test.ts | 10 +++----- packages/app/src/cli/services/info.ts | 30 ++++++++++------------ 2 files changed, 18 insertions(+), 22 deletions(-) diff --git a/packages/app/src/cli/services/info.test.ts b/packages/app/src/cli/services/info.test.ts index dc1ac758df2..6ca0aebc5b9 100644 --- a/packages/app/src/cli/services/info.test.ts +++ b/packages/app/src/cli/services/info.test.ts @@ -15,9 +15,7 @@ import {describe, expect, vi, test} from 'vitest' import {joinPath} from '@shopify/cli-kit/node/path' import {OutputMessage, TokenizedString, stringifyMessage, unstyled} from '@shopify/cli-kit/node/output' import {inTemporaryDirectory, writeFileSync} from '@shopify/cli-kit/node/fs' -import {InlineToken, renderInfo} from '@shopify/cli-kit/node/ui' - -type CustomSection = Exclude[0]['customSections'], undefined>[number] +import {AlertCustomSection, InlineToken} from '@shopify/cli-kit/node/ui' vi.mock('../prompts/dev.js') vi.mock('@shopify/cli-kit/node/node-package-manager') @@ -157,7 +155,7 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = await info(app, remoteApp, ORG1, infoOptions()) as CustomSection[] + const result = (await info(app, remoteApp, ORG1, infoOptions())) as AlertCustomSection[] const uiData = tabularDataSectionFromInfo(result, 'ui_extension_external') const checkoutData = tabularDataSectionFromInfo(result, 'checkout_ui_extension_external') @@ -205,7 +203,7 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = (await info(app, remoteApp, ORG1, infoOptions())) as CustomSection[] + const result = (await info(app, remoteApp, ORG1, infoOptions())) as AlertCustomSection[] const uiExtensionsData = tabularDataSectionFromInfo(result, 'ui_extension_external') const relevantExtension = extensionTitleRow(uiExtensionsData, 'handle-for-extension-1') const irrelevantExtension = extensionTitleRow(uiExtensionsData, 'point_of_sale') @@ -280,7 +278,7 @@ function mockApp({ }) } -function tabularDataSectionFromInfo(info: CustomSection[], title: string): InlineToken[][] { +function tabularDataSectionFromInfo(info: AlertCustomSection[], 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)) { diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 87856124279..cd8ff5f6643 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -14,11 +14,9 @@ import { shouldDisplayColors, stringifyMessage, } from '@shopify/cli-kit/node/output' -import {InlineToken, renderInfo} from '@shopify/cli-kit/node/ui' +import {AlertCustomSection, InlineToken} from '@shopify/cli-kit/node/ui' import {CLI_KIT_VERSION} from '@shopify/cli-kit/common/version' -type CustomSection = Exclude[0]['customSections'], undefined>[number] - export type Format = 'json' | 'text' export interface InfoOptions { format: Format @@ -33,7 +31,7 @@ export async function info( remoteApp: OrganizationApp, organization: Organization, options: InfoOptions, -): Promise { +): Promise { if (options.webEnv) { return infoWeb(app, remoteApp, organization, options) } else { @@ -54,7 +52,7 @@ async function infoApp( app: AppLinkedInterface, remoteApp: OrganizationApp, options: InfoOptions, -): Promise { +): Promise { if (options.format === 'json') { const extensionsInfo = withPurgedSchemas(app.allExtensions.filter((ext) => ext.isReturnedAsInfo())) let appWithSupportedExtensions = { @@ -124,7 +122,7 @@ class AppInfo { this.options = options } - async output(): Promise { + async output(): Promise { return [ ...(await this.devConfigsSection()), this.projectSettingsSection(), @@ -133,7 +131,7 @@ class AppInfo { ] } - async devConfigsSection(): Promise { + async devConfigsSection(): Promise { let updateUrls = NOT_CONFIGURED_TOKEN if (this.app.configuration.build?.automatically_update_urls_on_dev !== undefined) { updateUrls = this.app.configuration.build.automatically_update_urls_on_dev ? 'Yes' : 'No' @@ -170,11 +168,11 @@ class AppInfo { ] } - projectSettingsSection(): CustomSection { + projectSettingsSection(): AlertCustomSection { return this.tableSection('Your Project', [['Root location', {filePath: this.app.directory}]]) } - async appComponentsSection(): Promise { + async appComponentsSection(): Promise { const webComponentsSection = this.webComponentsSection() return [ { @@ -186,7 +184,7 @@ class AppInfo { ] } - webComponentsSection(): CustomSection | undefined { + webComponentsSection(): AlertCustomSection | undefined { const errors: OutputMessage[] = [] const sublevels: InlineToken[][] = [] if (!this.app.webs[0]) return @@ -220,11 +218,11 @@ class AppInfo { ]) } - extensionsSections(): CustomSection[] { + extensionsSections(): AlertCustomSection[] { const extensions = this.app.allExtensions.filter((ext) => ext.isReturnedAsInfo()) const types = Array.from(new Set(extensions.map((ext) => ext.type))) return types - .map((extensionType: string): CustomSection | undefined => { + .map((extensionType: string): AlertCustomSection | undefined => { const relevantExtensions = extensions.filter((extension: ExtensionInstance) => extension.type === extensionType) if (relevantExtensions[0]) { return this.subtableSection( @@ -233,7 +231,7 @@ class AppInfo { ) } }) - .filter((section: CustomSection | undefined) => section !== undefined) + .filter((section: AlertCustomSection | undefined) => section !== undefined) } extensionSubSection(extension: ExtensionInstance): InlineToken[][] { @@ -261,7 +259,7 @@ class AppInfo { return [`! ${errorFirstLine}`, ...errorRemainingLines.map((line) => ` ${line}`)].join('\n') } - async systemInfoSection(): Promise { + async systemInfoSection(): Promise { const {platform, arch} = platformAndArch() return this.tableSection('Tooling and System', [ ['Shopify CLI', CLI_KIT_VERSION], @@ -272,14 +270,14 @@ class AppInfo { ]) } - tableSection(title: string, rows: InlineToken[][], {isFirstItem = false} = {}): CustomSection { + tableSection(title: string, rows: InlineToken[][], {isFirstItem = false} = {}): AlertCustomSection { return { title: `${isFirstItem ? '' : '\n'}${title.toUpperCase()}\n`, body: {tabularData: rows, firstColumnSubdued: true}, } } - subtableSection(title: string, rows: InlineToken[][]): CustomSection { + subtableSection(title: string, rows: InlineToken[][]): AlertCustomSection { return { title, body: {tabularData: rows, firstColumnSubdued: true}, From 74298784759df70b0e82a94477fa816ec7d645f6 Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Mon, 13 Jan 2025 15:07:48 +0200 Subject: [PATCH 12/13] Suggest more updated command --- packages/app/src/cli/services/info.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index cd8ff5f6643..1b820227a9d 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -162,7 +162,7 @@ class AppInfo { { body: [ '💡 To change these, run', - {command: formatPackageManagerCommand(this.app.packageManager, 'dev', '--reset')}, + {command: formatPackageManagerCommand(this.app.packageManager, 'shopify app config link')}, ], }, ] From 7df0f33a77f1055e86c5a9edead3aa621e2c5dfd Mon Sep 17 00:00:00 2001 From: Ariel Caplan Date: Mon, 13 Jan 2025 18:24:51 +0200 Subject: [PATCH 13/13] Lint fix --- packages/app/src/cli/services/info.test.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/packages/app/src/cli/services/info.test.ts b/packages/app/src/cli/services/info.test.ts index 6ca0aebc5b9..21cbb059c06 100644 --- a/packages/app/src/cli/services/info.test.ts +++ b/packages/app/src/cli/services/info.test.ts @@ -107,7 +107,11 @@ describe('info', () => { vi.mocked(selectOrganizationPrompt).mockResolvedValue(ORG1) // When - const result = (await info(app, remoteApp, ORG1, {...infoOptions(), format: 'json', webEnv: true})) as OutputMessage + const result = (await info(app, remoteApp, ORG1, { + ...infoOptions(), + format: 'json', + webEnv: true, + })) as OutputMessage // Then expect(unstyled(stringifyMessage(result))).toMatchInlineSnapshot(`