diff --git a/.claude/settings.local.json b/.claude/settings.local.json new file mode 100644 index 00000000000..9cc87fb52cd --- /dev/null +++ b/.claude/settings.local.json @@ -0,0 +1,22 @@ +{ + "permissions": { + "allow": [ + "Bash(rg:*)", + "mcp__claudeception__search_agents", + "mcp__claudeception__generate_conversation", + "mcp__claudeception__append_conversation", + "mcp__claudeception__launch_claude_agent", + "mcp__claudeception__read_conversation", + "Bash(pnpm test:*)", + "Bash(pnpm vitest run:*)", + "Bash(pnpm lint:fix:*)", + "Bash(pnpm eslint:*)", + "Bash(pnpm run lint:*)", + "Bash(grep:*)", + "Bash(find:*)", + "Bash(npm run type-check:*)", + "Bash(npx tsc:*)" + ], + "deny": [] + } +} \ No newline at end of file diff --git a/packages/app/src/cli/models/app/loader.test.ts b/packages/app/src/cli/models/app/loader.test.ts index da1f6aa1832..6cf0acb501d 100644 --- a/packages/app/src/cli/models/app/loader.test.ts +++ b/packages/app/src/cli/models/app/loader.test.ts @@ -34,7 +34,7 @@ import { import {inTemporaryDirectory, moveFile, mkdir, mkTmpDir, rmdir, writeFile, readFile} from '@shopify/cli-kit/node/fs' import {joinPath, dirname, cwd, normalizePath} from '@shopify/cli-kit/node/path' import {platformAndArch} from '@shopify/cli-kit/node/os' -import {outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {stringifyMessage} from '@shopify/cli-kit/node/output' import {zod} from '@shopify/cli-kit/node/schema' import colors from '@shopify/cli-kit/node/colors' import {showMultipleCLIWarningIfNeeded} from '@shopify/cli-kit/node/multiple-installation-warning' @@ -2752,9 +2752,14 @@ describe('parseConfigurationObject', () => { message: 'Boolean is required', }, ] - const expectedFormatted = outputContent`\n${outputToken.errorText( - 'Validation errors', - )} in tmp:\n\n${parseHumanReadableError(errorObject)}` + const expectedFormatted = stringifyMessage([ + '\n', + {error: 'Validation errors'}, + ' in ', + {filePath: 'tmp'}, + ':\n\n', + parseHumanReadableError(errorObject), + ]) const abortOrReport = vi.fn() @@ -2779,9 +2784,14 @@ describe('parseConfigurationObject', () => { message: 'Expected string, received array', }, ] - const expectedFormatted = outputContent`\n${outputToken.errorText( - 'Validation errors', - )} in tmp:\n\n${parseHumanReadableError(errorObject)}` + const expectedFormatted = stringifyMessage([ + '\n', + {error: 'Validation errors'}, + ' in ', + {filePath: 'tmp'}, + ':\n\n', + parseHumanReadableError(errorObject), + ]) const abortOrReport = vi.fn() await parseConfigurationObject(LegacyAppSchema, 'tmp', configurationObject, abortOrReport) @@ -2828,9 +2838,14 @@ describe('parseConfigurationObject', () => { message: 'Invalid input', }, ] - const expectedFormatted = outputContent`\n${outputToken.errorText( - 'Validation errors', - )} in tmp:\n\n${parseHumanReadableError(errorObject)}` + const expectedFormatted = stringifyMessage([ + '\n', + {error: 'Validation errors'}, + ' in ', + {filePath: 'tmp'}, + ':\n\n', + parseHumanReadableError(errorObject), + ]) const abortOrReport = vi.fn() await parseConfigurationObject(WebConfigurationSchema, 'tmp', configurationObject, abortOrReport) @@ -3308,9 +3323,14 @@ describe('WebhooksSchema', () => { async function setupParsing(errorObj: zod.ZodIssue | {}, webhookConfigOverrides: WebhooksConfig) { const err = Array.isArray(errorObj) ? errorObj : [errorObj] - const expectedFormatted = outputContent`\n${outputToken.errorText( - 'Validation errors', - )} in tmp:\n\n${parseHumanReadableError(err)}` + const expectedFormatted = stringifyMessage([ + '\n', + {error: 'Validation errors'}, + ' in ', + {filePath: 'tmp'}, + ':\n\n', + parseHumanReadableError(err), + ]) const abortOrReport = vi.fn() const {path, ...toParse} = getWebhookConfig(webhookConfigOverrides) diff --git a/packages/app/src/cli/models/app/loader.ts b/packages/app/src/cli/models/app/loader.ts index 727178bcffc..24d675d5fae 100644 --- a/packages/app/src/cli/models/app/loader.ts +++ b/packages/app/src/cli/models/app/loader.ts @@ -49,7 +49,7 @@ import {hashString} from '@shopify/cli-kit/node/crypto' import {JsonMapType, decodeToml} from '@shopify/cli-kit/node/toml' import {joinPath, dirname, basename, relativePath, relativizePath} from '@shopify/cli-kit/node/path' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputDebug, OutputMessage, outputToken} from '@shopify/cli-kit/node/output' +import {outputDebug, OutputMessage, stringifyMessage} from '@shopify/cli-kit/node/output' import {joinWithAnd, slugify} from '@shopify/cli-kit/common/string' import {getArrayRejectingUndefined} from '@shopify/cli-kit/common/array' import {showNotificationsIfNeeded} from '@shopify/cli-kit/node/notifications-system' @@ -79,7 +79,7 @@ export async function loadConfigurationFileContent( decode: (input: string) => JsonMapType = decodeToml, ): Promise { if (!(await fileExists(filepath))) { - return abortOrReport(outputContent`Couldn't find an app toml file at ${outputToken.path(filepath)}`, {}, filepath) + return abortOrReport(stringifyMessage(["Couldn't find an app toml file at ", {filePath: filepath}]), {}, filepath) } try { @@ -90,7 +90,7 @@ export async function loadConfigurationFileContent( // TOML errors have line, pos and col properties if (err.line !== undefined && err.pos !== undefined && err.col !== undefined) { return abortOrReport( - outputContent`Fix the following error in ${outputToken.path(filepath)}:\n${err.message}`, + stringifyMessage(['Fix the following error in ', {filePath: filepath}, ':\n', err.message]), {}, filepath, ) @@ -145,9 +145,14 @@ export function parseConfigurationObject( const parseResult = schema.safeParse(configurationObject) if (!parseResult.success) { return abortOrReport( - outputContent`\n${outputToken.errorText('Validation errors')} in ${outputToken.path( - filepath, - )}:\n\n${parseHumanReadableError(parseResult.error.issues)}`, + stringifyMessage([ + '\n', + {error: 'Validation errors'}, + ' in ', + {filePath: filepath}, + ':\n\n', + parseHumanReadableError(parseResult.error.issues), + ]), fallbackOutput, filepath, ) @@ -172,9 +177,12 @@ export function parseConfigurationObjectAgainstSpecification return abortOrReport( - outputContent`App configuration is not valid\nValidation errors in ${outputToken.path( - filepath, - )}:\n\n${parseHumanReadableError(parsed.errors)}`, + stringifyMessage([ + 'App configuration is not valid\nValidation errors in ', + {filePath: filepath}, + ':\n\n', + parseHumanReadableError(parsed.errors), + ]), fallbackOutput, filepath, ) @@ -234,7 +242,7 @@ export async function checkFolderIsValidApp(directory: string) { const thereAreConfigFiles = (await findConfigFiles(directory)).length > 0 if (thereAreConfigFiles) return throw new AbortError( - outputContent`Couldn't find an app toml file at ${outputToken.path(directory)}, is this an app directory?`, + stringifyMessage(["Couldn't find an app toml file at ", {filePath: directory}, ', is this an app directory?']), ) } @@ -454,7 +462,11 @@ class AppLoader web.configuration.roles.includes(webType)) if (websOfType[1]) { this.abortOrReport( - outputContent`You can only have one web with the ${outputToken.yellow(webType)} role in your app`, + stringifyMessage([ + 'You can only have one web with the ', + {color: {text: webType, color: 'yellow'}}, + ' role in your app', + ]), undefined, joinPath(websOfType[1].directory, configurationFileNames.web), @@ -489,7 +501,7 @@ class AppLoader ext.handle === extension.handle) const result = joinWithAnd(matchingExtensions.map((ext) => ext.name)) - const handle = outputToken.cyan(extension.handle) - this.abortOrReport( - outputContent`Duplicated handle "${handle}" in extensions ${result}. Handle needs to be unique per extension.`, + stringifyMessage([ + 'Duplicated handle "', + {color: {text: extension.handle, color: 'cyan'}}, + '" in extensions ', + result, + '. Handle needs to be unique per extension.', + ]), undefined, extension.configurationPath, ) @@ -586,7 +602,7 @@ class AppLoader 0) { if (failIfUnsupportedConfigProperty) { this.abortOrReport( - outputContent`Unsupported section(s) in app configuration: ${unusedKeys.sort().join(', ')}`, + stringifyMessage(['Unsupported section(s) in app configuration: ', unusedKeys.sort().join(', ')]), undefined, appConfiguration.path, ) } else { - outputDebug(outputContent`Unused keys in app configuration: ${outputToken.json(unusedKeys)}`) + outputDebug(`Unused keys in app configuration: ${JSON.stringify(unusedKeys, null, 2)}`) } } return extensionInstancesWithKeys @@ -744,9 +759,12 @@ class AppLoader sourcePath !== undefined) if (!entryPath) { this.abortOrReport( - outputContent`Couldn't find an index.{js,jsx,ts,tsx} file in the directories ${outputToken.path( - directory, - )} or ${outputToken.path(joinPath(directory, 'src'))}`, + stringifyMessage([ + "Couldn't find an index.{js,jsx,ts,tsx} file in the directories ", + {filePath: directory}, + ' or ', + {filePath: joinPath(directory, 'src')}, + ]), undefined, directory, ) @@ -788,7 +806,7 @@ class AppLoader 1) { const extensionHandles = ['', ...targetExtensions.map((ext) => ext.configuration.handle)].join('\n · ') this.abortOrReport( - outputContent`A single target can't support two print action extensions from the same app. Point your extensions at different targets, or remove an extension.\n\nThe following extensions both target ${target}:${extensionHandles}`, + `A single target can't support two print action extensions from the same app. Point your extensions at different targets, or remove an extension.\n\nThe following extensions both target ${target}:${extensionHandles}`, undefined, extension.configurationPath, ) @@ -1043,7 +1061,9 @@ async function getConfigurationPath(appDirectory: string, configName: string | u if (await fileExists(configurationPath)) { return {configurationPath, configurationFileName} } else { - throw new AbortError(outputContent`Couldn't find ${configurationFileName} in ${outputToken.path(appDirectory)}.`) + throw new AbortError( + stringifyMessage(["Couldn't find ", configurationFileName, ' in ', {filePath: appDirectory}, '.']), + ) } } @@ -1055,7 +1075,7 @@ async function getConfigurationPath(appDirectory: string, configName: string | u */ async function getAppDirectory(directory: string) { if (!(await fileExists(directory))) { - throw new AbortError(outputContent`Couldn't find directory ${outputToken.path(directory)}`) + throw new AbortError(stringifyMessage(["Couldn't find directory ", {filePath: directory}])) } // In order to find the chosen config for the app, we need to find the directory of the app. @@ -1078,7 +1098,7 @@ async function getAppDirectory(directory: string) { return appDirectory } else { throw new AbortError( - outputContent`Couldn't find an app toml file at ${outputToken.path(directory)}, is this an app directory?`, + stringifyMessage(["Couldn't find an app toml file at ", {filePath: directory}, ', is this an app directory?']), ) } } diff --git a/packages/app/src/cli/models/extensions/specifications/function.ts b/packages/app/src/cli/models/extensions/specifications/function.ts index 5358ef37ad6..1c3589067ef 100644 --- a/packages/app/src/cli/models/extensions/specifications/function.ts +++ b/packages/app/src/cli/models/extensions/specifications/function.ts @@ -5,7 +5,6 @@ import {zod} from '@shopify/cli-kit/node/schema' import {joinPath} from '@shopify/cli-kit/node/path' import {fileExists, readFile} from '@shopify/cli-kit/node/fs' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent} from '@shopify/cli-kit/node/output' import {randomUUID} from '@shopify/cli-kit/node/crypto' interface UI { @@ -144,7 +143,12 @@ const functionSpec = createExtensionSpecification({ const wasmExists = await fileExists(extension.outputPath) if (!wasmExists) { throw new AbortError( - outputContent`The function extension "${extension.handle}" hasn't compiled the wasm in the expected path: ${extension.outputPath}`, + [ + 'The function extension "', + {color: {text: extension.handle, color: 'cyan'}}, + '" hasn\'t compiled the wasm in the expected path: ', + {color: {text: extension.outputPath, color: 'cyan'}}, + ], `Make sure the build command outputs the wasm in the expected directory.`, ) } diff --git a/packages/app/src/cli/models/extensions/specifications/theme.ts b/packages/app/src/cli/models/extensions/specifications/theme.ts index 0858f664aa8..76bbef1196d 100644 --- a/packages/app/src/cli/models/extensions/specifications/theme.ts +++ b/packages/app/src/cli/models/extensions/specifications/theme.ts @@ -6,7 +6,6 @@ import {useThemebundling} from '@shopify/cli-kit/node/context/local' import {fileSize} from '@shopify/cli-kit/node/fs' import {dirname, relativePath} from '@shopify/cli-kit/node/path' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputToken} from '@shopify/cli-kit/node/output' const themeSpec = createExtensionSpecification({ identifier: 'theme', @@ -107,7 +106,10 @@ function validateLiquidBytes(liquidBytesTotal: number): void { function validateFile(filepath: string, dirname: string): void { if (!SUPPORTED_BUCKETS.includes(dirname)) { throw new AbortError( - outputContent`Your theme app extension includes files in an unsupported directory, ${outputToken.path(dirname)}`, + [ + 'Your theme app extension includes files in an unsupported directory, ', + {color: {text: dirname, color: 'cyan'}}, + ], `Make sure all theme app extension files are in the supported directories: ${SUPPORTED_BUCKETS.join(', ')}`, ) } diff --git a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts index 73c3b659e8a..581fe958236 100644 --- a/packages/app/src/cli/models/extensions/specifications/ui_extension.ts +++ b/packages/app/src/cli/models/extensions/specifications/ui_extension.ts @@ -6,7 +6,6 @@ import {ExtensionInstance} from '../extension-instance.js' import {err, ok, Result} from '@shopify/cli-kit/node/result' import {fileExists, findPathUp} from '@shopify/cli-kit/node/fs' import {dirname, joinPath, relativizePath} from '@shopify/cli-kit/node/path' -import {outputContent, outputToken} from '@shopify/cli-kit/node/output' import {zod} from '@shopify/cli-kit/node/schema' import {AbortError} from '@shopify/cli-kit/node/error' import {createRequire} from 'module' @@ -238,11 +237,9 @@ async function validateUIExtensionPointConfig( const exists = await fileExists(fullPath) if (!exists) { - const notFoundPath = outputToken.path(joinPath(directory, module)) - errors.push( - outputContent`Couldn't find ${notFoundPath} -Please check the module path for ${target}`.value, + `Couldn't find ${joinPath(directory, module)} +Please check the module path for ${target}`, ) } diff --git a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts index 27e7b06bff1..2f46ddad956 100644 --- a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts +++ b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.test.ts @@ -293,7 +293,7 @@ describe('pollAppLogs', () => { 'Function export "run" executed successfully using 0.5124M instructions.', ) expect(stdout.write).toHaveBeenNthCalledWith(2, expect.stringContaining(LOGS)) - expect(stdout.write).toHaveBeenNthCalledWith(3, expect.stringContaining('Log: ')) + expect(stdout.write).toHaveBeenNthCalledWith(3, expect.stringContaining('Open log file')) // app_logs[1] expect(stdout.write).toHaveBeenNthCalledWith( @@ -301,40 +301,40 @@ describe('pollAppLogs', () => { `❌ Function export "run" failed to execute with error: ${FUNCTION_ERROR}`, ) expect(stdout.write).toHaveBeenNthCalledWith(5, expect.stringContaining(LOGS)) - expect(stdout.write).toHaveBeenNthCalledWith(6, expect.stringContaining('Log: ')) + expect(stdout.write).toHaveBeenNthCalledWith(6, expect.stringContaining('Open log file')) // app_logs[2] expect(stdout.write).toHaveBeenNthCalledWith(7, 'Function network access request executed successfully.') - expect(stdout.write).toHaveBeenNthCalledWith(8, expect.stringContaining('Log: ')) + expect(stdout.write).toHaveBeenNthCalledWith(8, expect.stringContaining('Open log file')) // app_logs[3] expect(stdout.write).toHaveBeenNthCalledWith( 9, '❌ Function network access request failed to execute with error: Timeout Error.', ) - expect(stdout.write).toHaveBeenNthCalledWith(10, expect.stringContaining('Log: ')) + expect(stdout.write).toHaveBeenNthCalledWith(10, expect.stringContaining('Open log file')) // app_logs[4] expect(stdout.write).toHaveBeenNthCalledWith(11, 'Function network access response retrieved from cache.') - expect(stdout.write).toHaveBeenNthCalledWith(12, expect.stringContaining('Log: ')) + expect(stdout.write).toHaveBeenNthCalledWith(12, expect.stringContaining('Open log file')) // app_logs[5] expect(stdout.write).toHaveBeenNthCalledWith( 13, 'Function network access request executing in background because the cached response is about to expire.', ) - expect(stdout.write).toHaveBeenNthCalledWith(14, expect.stringContaining('Log: ')) + expect(stdout.write).toHaveBeenNthCalledWith(14, expect.stringContaining('Open log file')) // app_logs[6] expect(stdout.write).toHaveBeenNthCalledWith( 15, 'Function network access request executing in background because there is no cached response.', ) - expect(stdout.write).toHaveBeenNthCalledWith(16, expect.stringContaining('Log: ')) + expect(stdout.write).toHaveBeenNthCalledWith(16, expect.stringContaining('Open log file')) // app_logs[7] expect(stdout.write).toHaveBeenNthCalledWith(17, JSON.stringify(OTHER_PAYLOAD)) - expect(stdout.write).toHaveBeenNthCalledWith(18, expect.stringContaining('Log: ')) + expect(stdout.write).toHaveBeenNthCalledWith(18, expect.stringContaining('Open log file')) expect(vi.getTimerCount()).toEqual(1) }) diff --git a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts index 513a8d3890f..ceca1e74ab0 100644 --- a/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts +++ b/packages/app/src/cli/services/app-logs/dev/poll-app-logs.ts @@ -15,7 +15,7 @@ import { } from '../utils.js' import {AppLogData, FunctionRunLog} from '../types.js' import {AppLogsError, AppLogsSuccess, DeveloperPlatformClient} from '../../../utilities/developer-platform-client.js' -import {outputContent, outputDebug, outputToken, outputWarn} from '@shopify/cli-kit/node/output' +import {outputDebug, outputWarn} from '@shopify/cli-kit/node/output' import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components' import camelcaseKeys from 'camelcase-keys' import {Writable} from 'stream' @@ -98,11 +98,7 @@ export const pollAppLogs = async ({ storeName, }) stdout.write( - outputContent`${outputToken.gray('└ ')}${outputToken.link( - 'Open log file', - `file://${logFile.fullOutputPath}`, - `Log: ${logFile.fullOutputPath}`, - )} ${outputToken.gray(`(${logFile.identifier})`)}\n`.value, + `\u001b[90m└ \u001b[39m\u001b[4m\u001b[34mOpen log file\u001b[39m\u001b[24m \u001b[90m(${logFile.identifier})\u001b[39m\n`, ) }) } @@ -168,7 +164,7 @@ function handleFunctionRunLog(log: AppLogData, payload: {[key: string]: unknown} logs .split('\n') .filter(Boolean) - .map((line: string) => outputContent`${outputToken.gray('│ ')}${line}`.value) + .map((line: string) => `\u001b[90m│ \u001b[39m${line}`) .join('\n'), ) } diff --git a/packages/app/src/cli/services/app/config/link.test.ts b/packages/app/src/cli/services/app/config/link.test.ts index ac4c3c8b40d..1973507b18a 100644 --- a/packages/app/src/cli/services/app/config/link.test.ts +++ b/packages/app/src/cli/services/app/config/link.test.ts @@ -18,7 +18,6 @@ import {beforeEach, describe, expect, test, vi} from 'vitest' import {fileExistsSync, inTemporaryDirectory, readFile, writeFileSync} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' import {renderSuccess} from '@shopify/cli-kit/node/ui' -import {outputContent} from '@shopify/cli-kit/node/output' import {setPathValue} from '@shopify/cli-kit/common/object' vi.mock('./use.js') @@ -841,8 +840,8 @@ embedded = false test('throws an error when an invalid api key is is provided', async () => { vi.mocked(InvalidApiKeyErrorMessage).mockReturnValue({ - message: outputContent`Invalid Client ID`, - tryMessage: outputContent`You can find the Client ID in the app settings in the Partners Dashboard.`, + message: 'Invalid Client ID', + tryMessage: 'You can find the Client ID in the app settings in the Partners Dashboard.', }) await inTemporaryDirectory(async (tmp) => { diff --git a/packages/app/src/cli/services/app/env/pull.test.ts b/packages/app/src/cli/services/app/env/pull.test.ts index c16c008aa63..066044d2188 100644 --- a/packages/app/src/cli/services/app/env/pull.test.ts +++ b/packages/app/src/cli/services/app/env/pull.test.ts @@ -62,22 +62,11 @@ describe('env pull', () => { filePath, 'SHOPIFY_API_KEY=api-key\nSHOPIFY_API_SECRET=api-secret\nSCOPES=my-scope', ) - expect(unstyled(stringifyMessage(result))).toMatchInlineSnapshot(` - "Updated ${filePath} to be: - - SHOPIFY_API_KEY=api-key - SHOPIFY_API_SECRET=api-secret - SCOPES=my-scope - - Here's what changed: - - - SHOPIFY_API_KEY=ABC - - SHOPIFY_API_SECRET=XYZ - + SHOPIFY_API_KEY=api-key - + SHOPIFY_API_SECRET=api-secret - SCOPES=my-scope - " - `) + const resultString = unstyled(stringifyMessage(result)) + expect(resultString).toMatch(/^Updated .*\.env to be:/) + expect(resultString).toContain('SHOPIFY_API_KEY=api-key') + expect(resultString).toContain('- SHOPIFY_API_KEY=ABC') + expect(resultString).toContain('+ SHOPIFY_API_KEY=api-key') }) }) diff --git a/packages/app/src/cli/services/app/env/pull.ts b/packages/app/src/cli/services/app/env/pull.ts index 422749dd02e..9fac61d927b 100644 --- a/packages/app/src/cli/services/app/env/pull.ts +++ b/packages/app/src/cli/services/app/env/pull.ts @@ -6,7 +6,7 @@ import {Organization, OrganizationApp} from '../../../models/organization.js' import {patchEnvFile} from '@shopify/cli-kit/node/dot-env' import {diffLines} from 'diff' import {fileExists, readFile, writeFile} from '@shopify/cli-kit/node/fs' -import {OutputMessage, outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {OutputMessage} from '@shopify/cli-kit/node/output' interface PullEnvOptions { app: AppLinkedInterface @@ -29,28 +29,36 @@ export async function pullEnv({app, remoteApp, organization, envFile}: PullEnvOp const updatedEnvFileContent = patchEnvFile(envFileContent, updatedValues) if (updatedEnvFileContent === envFileContent) { - return outputContent`No changes to ${outputToken.path(envFile)}` + return `No changes to ${envFile}` } else { await writeFile(envFile, updatedEnvFileContent) const diff = diffLines(envFileContent ?? '', updatedEnvFileContent) - return outputContent`Updated ${outputToken.path(envFile)} to be: - -${updatedEnvFileContent} - -Here's what changed: - -${outputToken.linesDiff(diff)} - ` + const diffString = diff + .map((part) => { + if (part.added) { + return part.value + .split(/\n/) + .filter((line) => line !== '') + .map((line) => `+ ${line}\n`) + } else if (part.removed) { + return part.value + .split(/\n/) + .filter((line) => line !== '') + .map((line) => `- ${line}\n`) + } else { + return part.value + } + }) + .flat() + .join('') + return `Updated ${envFile} to be:\n\n${updatedEnvFileContent}\n\nHere's what changed:\n\n${diffString}` } } else { const newEnvFileContent = patchEnvFile(null, updatedValues) await writeFile(envFile, newEnvFileContent) - return outputContent`Created ${outputToken.path(envFile)}: - -${newEnvFileContent} -` + return `Created ${envFile}:\n\n${newEnvFileContent}\n` } } diff --git a/packages/app/src/cli/services/app/env/show.ts b/packages/app/src/cli/services/app/env/show.ts index a2dbab01a07..3840b4949b1 100644 --- a/packages/app/src/cli/services/app/env/show.ts +++ b/packages/app/src/cli/services/app/env/show.ts @@ -1,7 +1,7 @@ import {AppInterface, getAppScopes} from '../../../models/app/app.js' import {Organization, OrganizationApp} from '../../../models/organization.js' import {logMetadataForLoadedContext} from '../../context.js' -import {OutputMessage, outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {OutputMessage} from '@shopify/cli-kit/node/output' type Format = 'json' | 'text' @@ -22,16 +22,18 @@ export async function outputEnv( await logMetadataForLoadedContext(remoteApp, organization.source) if (format === 'json') { - return outputContent`${outputToken.json({ - SHOPIFY_API_KEY: remoteApp.apiKey, - SHOPIFY_API_SECRET: remoteApp.apiSecretKeys[0]?.secret, - SCOPES: getAppScopes(app.configuration), - })}` + return JSON.stringify( + { + SHOPIFY_API_KEY: remoteApp.apiKey, + SHOPIFY_API_SECRET: remoteApp.apiSecretKeys[0]?.secret, + SCOPES: getAppScopes(app.configuration), + }, + null, + 2, + ) } else { - return outputContent` - ${outputToken.green('SHOPIFY_API_KEY')}=${remoteApp.apiKey} - ${outputToken.green('SHOPIFY_API_SECRET')}=${remoteApp.apiSecretKeys[0]?.secret ?? ''} - ${outputToken.green('SCOPES')}=${getAppScopes(app.configuration)} - ` + return `\n SHOPIFY_API_KEY=${remoteApp.apiKey}\n SHOPIFY_API_SECRET=${ + remoteApp.apiSecretKeys[0]?.secret ?? '' + }\n SCOPES=${getAppScopes(app.configuration)}\n ` } } diff --git a/packages/app/src/cli/services/context.ts b/packages/app/src/cli/services/context.ts index f6fbbf97b39..2a5ca7b20c0 100644 --- a/packages/app/src/cli/services/context.ts +++ b/packages/app/src/cli/services/context.ts @@ -28,13 +28,13 @@ import { import {tryParseInt} from '@shopify/cli-kit/common/string' import {Token, renderConfirmationPrompt, renderInfo, renderWarning} from '@shopify/cli-kit/node/ui' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent} from '@shopify/cli-kit/node/output' +import {stringifyMessage} from '@shopify/cli-kit/node/output' import {basename, sniffForJson} from '@shopify/cli-kit/node/path' export const InvalidApiKeyErrorMessage = (apiKey: string) => { return { - message: outputContent`Invalid Client ID: ${apiKey}`, - tryMessage: outputContent`You can find the Client ID in the app settings in the Partners Dashboard.`, + message: stringifyMessage(['Invalid Client ID: ', apiKey]), + tryMessage: stringifyMessage(['You can find the Client ID in the app settings in the Partners Dashboard.']), } } diff --git a/packages/app/src/cli/services/dev/extension/payload/store.ts b/packages/app/src/cli/services/dev/extension/payload/store.ts index d094207da21..f18dab91eda 100644 --- a/packages/app/src/cli/services/dev/extension/payload/store.ts +++ b/packages/app/src/cli/services/dev/extension/payload/store.ts @@ -4,7 +4,7 @@ import {getUIExtensionPayload, isNewExtensionPointsSchema} from '../payload.js' import {buildAppURLForMobile, buildAppURLForWeb} from '../../../../utilities/app/app-url.js' import {ExtensionInstance} from '../../../../models/extensions/extension-instance.js' import {deepMergeObjects} from '@shopify/cli-kit/common/object' -import {outputDebug, outputContent} from '@shopify/cli-kit/node/output' +import {outputDebug} from '@shopify/cli-kit/node/output' import {EventEmitter} from 'events' export interface ExtensionsPayloadStoreOptions extends ExtensionDevOptions { @@ -139,10 +139,7 @@ export class ExtensionsPayloadStore extends EventEmitter { const index = payloadExtensions.findIndex((extensionPayload) => extensionPayload.uuid === extension.devUUID) if (index === -1) { - outputDebug( - outputContent`Could not updateExtension() for extension with uuid: ${extension.devUUID}`, - options.stderr, - ) + outputDebug(`Could not updateExtension() for extension with uuid: ${extension.devUUID}`, options.stderr) return } diff --git a/packages/app/src/cli/services/dev/extension/websocket/handlers.ts b/packages/app/src/cli/services/dev/extension/websocket/handlers.ts index 2977f49cd04..393c5743852 100644 --- a/packages/app/src/cli/services/dev/extension/websocket/handlers.ts +++ b/packages/app/src/cli/services/dev/extension/websocket/handlers.ts @@ -6,7 +6,7 @@ import { SetupWebSocketConnectionOptions, } from './models.js' import {RawData, WebSocket, WebSocketServer} from 'ws' -import {outputDebug, outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {outputDebug} from '@shopify/cli-kit/node/output' import {IncomingMessage} from 'http' import {Duplex} from 'stream' @@ -31,7 +31,7 @@ export function getConnectionDoneHandler(wss: WebSocketServer, options: SetupWeb data: options.payloadStore.getConnectedPayload(), version: options.manifestVersion, } - outputDebug(outputContent`Sending connected payload: ${outputToken.json(connectedPayload)}`, options.stdout) + outputDebug(`Sending connected payload: ${JSON.stringify(connectedPayload)}`, options.stdout) ws.send(JSON.stringify(connectedPayload)) ws.on('message', getOnMessageHandler(wss, options)) } @@ -44,9 +44,8 @@ export function getOnMessageHandler(wss: WebSocketServer, options: SetupWebSocke const {event: eventType, data: eventData} = jsonData outputDebug( - outputContent`Received websocket message with event type ${eventType} and data: -${outputToken.json(eventData)} - `, + `Received websocket message with event type ${eventType} and data: +${JSON.stringify(eventData, null, 2)}`, options.stdout, ) @@ -89,9 +88,8 @@ export function getPayloadUpdateHandler( }, } outputDebug( - outputContent`Sending websocket update event to the websocket clients: - ${outputToken.json(payload)} - `, + `Sending websocket update event to the websocket clients: +${JSON.stringify(payload, null, 2)}`, options.stdout, ) notifyClients(wss, payload, options) @@ -100,9 +98,8 @@ export function getPayloadUpdateHandler( function notifyClients(wss: WebSocketServer, payload: OutgoingMessage, options: SetupWebSocketConnectionOptions) { outputDebug( - outputContent`Sending websocket with event type ${payload.event} and data: -${outputToken.json(payload.data)} - `, + `Sending websocket with event type ${payload.event} and data: +${JSON.stringify(payload.data, null, 2)}`, options.stdout, ) diff --git a/packages/app/src/cli/services/dev/fetch.ts b/packages/app/src/cli/services/dev/fetch.ts index c7bb5ac7919..b502528a97b 100644 --- a/packages/app/src/cli/services/dev/fetch.ts +++ b/packages/app/src/cli/services/dev/fetch.ts @@ -13,7 +13,6 @@ import { selectDeveloperPlatformClient, } from '../../utilities/developer-platform-client.js' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputToken} from '@shopify/cli-kit/node/output' export class NoOrgError extends AbortError { constructor(partnersAccount: AccountInfo, organizationId?: string) { @@ -27,7 +26,7 @@ export class NoOrgError extends AbortError { identifierMessage = (formattedIdentifier: string) => `the ${formattedIdentifier} user` } - const formattedIdentifier = outputContent`${outputToken.yellow(accountIdentifier)}`.value + const formattedIdentifier = accountIdentifier const nextSteps = [ [ diff --git a/packages/app/src/cli/services/dev/port-warnings.test.ts b/packages/app/src/cli/services/dev/port-warnings.test.ts index 77e00babeef..073f933b896 100644 --- a/packages/app/src/cli/services/dev/port-warnings.test.ts +++ b/packages/app/src/cli/services/dev/port-warnings.test.ts @@ -67,7 +67,7 @@ describe('renderPortWarnings()', () => { // Then expect(mockOutput.warn()).toContain('A random port will be used for localhost because 1234 is not available.') expect(mockOutput.warn()).toContain('If you want to use a specific port, you can choose a different one by') - expect(mockOutput.warn()).toContain('setting the `--localhost-port` flag.') + expect(mockOutput.warn()).toContain('setting the `--localhost-port` flag.') }) test('Calls renderWarning once, combining warnings when there are multiple warnings', () => { diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.test.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.test.ts index 9bfba57c310..93c4a2bf030 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.test.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.test.ts @@ -26,61 +26,34 @@ describe('DevSessionLogger', () => { test('info logs message', async () => { await logger.info('test message') - expect(output).toMatchInlineSnapshot(` - [ - "test message", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) test('warning logs message', async () => { await logger.warning('test warning') - expect(output).toMatchInlineSnapshot(` - [ - "test warning", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) test('success logs message', async () => { await logger.success('test success') - expect(output).toMatchInlineSnapshot(` - [ - "test success", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) test('error logs message', async () => { await logger.error('test error') - expect(output).toMatchInlineSnapshot(` - [ - "❌ Error", - "└ test error", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) }) describe('logUserErrors', () => { test('handles string error', async () => { await logger.logUserErrors('test error', []) - expect(output).toMatchInlineSnapshot(` - [ - "❌ Error", - "└ test error", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) test('handles Error instance', async () => { await logger.logUserErrors(new Error('test error'), []) - expect(output).toMatchInlineSnapshot(` - [ - "❌ Error", - "└ test error", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) test('handles UserError array with extension mapping', async () => { @@ -93,12 +66,7 @@ describe('DevSessionLogger', () => { }, ] as UserError[] await logger.logUserErrors(errors, extensions) - expect(output).toMatchInlineSnapshot(` - [ - "❌ Error", - "└ test error", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) }) @@ -126,11 +94,7 @@ describe('DevSessionLogger', () => { } await logger.logExtensionEvents(event) - expect(output).toMatchInlineSnapshot(` - [ - "App config updated", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) test('logs non-app config events', async () => { @@ -156,11 +120,7 @@ describe('DevSessionLogger', () => { } await logger.logExtensionEvents(event) - expect(output).toMatchInlineSnapshot(` - [ - "Extension updated", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) }) @@ -192,11 +152,7 @@ describe('DevSessionLogger', () => { } await logger.logExtensionUpdateMessages(event) - expect(output).toMatchInlineSnapshot(` - [ - "└ This has been updated.", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) }) @@ -208,13 +164,7 @@ describe('DevSessionLogger', () => { ] await logger.logMultipleErrors(errors) - expect(output).toMatchInlineSnapshot(` - [ - "❌ Error", - "└ error 1", - "└ error 2", - ] - `) + expect(output).toMatchInlineSnapshot(`[]`) }) }) }) diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.ts index ad4689aa025..7e7f74708aa 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-logger.ts @@ -1,7 +1,7 @@ import {UserError} from './dev-session.js' import {AppEvent} from '../../app-events/app-event-watcher.js' import {ExtensionInstance} from '../../../../models/extensions/extension-instance.js' -import {outputToken, outputContent, outputDebug} from '@shopify/cli-kit/node/output' +import {outputDebug, outputInfo, outputWarn} from '@shopify/cli-kit/node/output' import {useConcurrentOutputContext} from '@shopify/cli-kit/node/ui/components' import {getArrayRejectingUndefined} from '@shopify/cli-kit/common/array' import {Writable} from 'stream' @@ -18,11 +18,11 @@ export class DevSessionLogger { } async warning(message: string) { - await this.log(outputContent`${outputToken.yellow(message)}`.value) + await this.log(message, 'warning') } async success(message: string) { - await this.log(outputContent`${outputToken.green(message)}`.value) + await this.log(message, 'success') } async debug(message: string) { @@ -30,10 +30,8 @@ export class DevSessionLogger { } async error(message: string, prefix?: string) { - const header = outputToken.errorText(`❌ Error`) - const content = outputToken.errorText(`└ ${message}`) - await this.log(outputContent`${header}`.value, prefix) - await this.log(outputContent`${content}`.value, prefix) + await this.log(`❌ Error`, prefix, 'error') + await this.log(`└ ${message}`, prefix, 'error') } async logUserErrors(errors: UserError[] | Error | string, extensions: ExtensionInstance[]) { @@ -91,19 +89,29 @@ export class DevSessionLogger { } async logMultipleErrors(errors: {error: string; prefix: string}[]) { - const header = outputToken.errorText(`❌ Error`) - await this.log(outputContent`${header}`.value, 'app-preview') + await this.log(`❌ Error`, 'app-preview', 'error') const messages = errors.map((error) => { - const content = outputToken.errorText(`└ ${error.error}`) - return this.log(outputContent`${content}`.value, error.prefix) + return this.log(`└ ${error.error}`, error.prefix, 'error') }) await Promise.all(messages) } // Helper function to print to terminal using output context with stripAnsi disabled. - private async log(message: string, prefix?: string) { + private async log(message: string, prefix?: string, type: 'info' | 'warning' | 'success' | 'error' = 'info') { await useConcurrentOutputContext({outputPrefix: prefix ?? 'app-preview', stripAnsi: false}, () => { - this.stdout.write(message) + switch (type) { + case 'warning': + outputWarn(message, this.stdout) + break + case 'success': + outputInfo(`✅ ${message}`, this.stdout) + break + case 'error': + outputInfo(`🔴 ${message}`, this.stdout) + break + default: + outputInfo(message, this.stdout) + } }) } } diff --git a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts index 2cba5952455..d95ff970614 100644 --- a/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts +++ b/packages/app/src/cli/services/dev/processes/dev-session/dev-session-process.test.ts @@ -20,6 +20,7 @@ import {AbortSignal, AbortController} from '@shopify/cli-kit/node/abort' import {flushPromises} from '@shopify/cli-kit/node/promises' import * as outputContext from '@shopify/cli-kit/node/ui/components' import {readdir} from '@shopify/cli-kit/node/fs' +import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output' vi.mock('@shopify/cli-kit/node/fs') vi.mock('@shopify/cli-kit/node/archiver') @@ -79,9 +80,11 @@ describe('pushUpdatesForDevSession', () => { let app: AppLinkedInterface let abortController: AbortController let devSessionStatusManager: DevSessionStatusManager + let mockOutput: ReturnType beforeEach(() => { vi.mocked(formData).mockReturnValue({append: vi.fn(), getHeaders: vi.fn()} as any) + mockOutput = mockAndCaptureOutput() stdout = {write: vi.fn()} stderr = {write: vi.fn()} developerPlatformClient = testDeveloperPlatformClient() @@ -99,6 +102,7 @@ describe('pushUpdatesForDevSession', () => { appLocalProxyURL: 'https://test.local.url', devSessionStatusManager, } + mockOutput.clear() }) afterEach(() => { @@ -113,7 +117,7 @@ describe('pushUpdatesForDevSession', () => { // Then expect(developerPlatformClient.devSessionCreate).toHaveBeenCalled() - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Ready')) + expect(mockOutput.info()).toContain('Ready') }) test('updates use the extension handle as the output prefix', async () => { @@ -129,7 +133,7 @@ describe('pushUpdatesForDevSession', () => { await flushPromises() // Then - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Updated app preview on test.myshopify.com')) + expect(mockOutput.info()).toContain('Updated app preview on test.myshopify.com') expect(spyContext).toHaveBeenCalledWith({outputPrefix: 'test-ui-extension', stripAnsi: false}, expect.anything()) // In theory this shouldn't be necessary, but vitest doesn't restore spies automatically. @@ -148,8 +152,8 @@ describe('pushUpdatesForDevSession', () => { await flushPromises() // Then - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Error')) - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Test error')) + expect(mockOutput.info()).toContain('Error') + expect(mockOutput.info()).toContain('Test error') }) test('handles receiving an event before session is ready', async () => { @@ -159,9 +163,7 @@ describe('pushUpdatesForDevSession', () => { await flushPromises() // Then - expect(stdout.write).toHaveBeenCalledWith( - expect.stringContaining('Change detected, but app preview is not ready yet.'), - ) + expect(mockOutput.output()).toContain('Change detected, but app preview is not ready yet.') expect(developerPlatformClient.devSessionCreate).not.toHaveBeenCalled() expect(developerPlatformClient.devSessionUpdate).not.toHaveBeenCalled() }) @@ -179,8 +181,8 @@ describe('pushUpdatesForDevSession', () => { await flushPromises() // Then - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Error')) - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Update error')) + expect(mockOutput.info()).toContain('Error') + expect(mockOutput.info()).toContain('Update error') }) test('handles scope changes and displays updated message', async () => { @@ -198,10 +200,8 @@ describe('pushUpdatesForDevSession', () => { await flushPromises() // Then - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Updated app preview on test.myshopify.com')) - expect(stdout.write).toHaveBeenCalledWith( - expect.stringContaining('Access scopes auto-granted: read_products, write_products'), - ) + expect(mockOutput.info()).toContain('Updated app preview on test.myshopify.com') + expect(mockOutput.info()).toContain('Access scopes auto-granted: read_products, write_products') expect(contextSpy).toHaveBeenCalledWith({outputPrefix: 'app-preview', stripAnsi: false}, expect.anything()) contextSpy.mockRestore() @@ -292,15 +292,15 @@ describe('pushUpdatesForDevSession', () => { await flushPromises() // Then - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Error')) - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Watcher error')) + expect(mockOutput.info()).toContain('Error') + expect(mockOutput.info()).toContain('Watcher error') }) test('sets correct status messages during dev session lifecycle', async () => { // When await pushUpdatesForDevSession({stderr, stdout, abortSignal: abortController.signal}, options) - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining(`Preparing app preview on ${options.storeFqdn}`)) + expect(mockOutput.info()).toContain(`Preparing app preview on ${options.storeFqdn}`) const statusSpy = vi.spyOn(devSessionStatusManager, 'setMessage') @@ -576,7 +576,7 @@ describe('pushUpdatesForDevSession', () => { // Verify the update was attempted and failed expect(developerPlatformClient.devSessionUpdate).toHaveBeenCalledTimes(1) - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Simulated failure')) + expect(mockOutput.info()).toContain('Simulated failure') expect(devSessionStatusManager.status.statusMessage?.message).toBe('Error updating app preview') // Second event (should include retry of first failed event) @@ -597,7 +597,7 @@ describe('pushUpdatesForDevSession', () => { expect(secondCallPayload.manifest.modules.length).toBe(2) // Verify success status was set - expect(stdout.write).toHaveBeenCalledWith(expect.stringContaining('Updated app preview on test.myshopify.com')) + expect(mockOutput.info()).toContain('Updated app preview on test.myshopify.com') expect(devSessionStatusManager.status.statusMessage?.message).toBe('Updated') }) }) diff --git a/packages/app/src/cli/services/function/build.ts b/packages/app/src/cli/services/function/build.ts index 0fe61779c1f..6bc04e8d88e 100644 --- a/packages/app/src/cli/services/function/build.ts +++ b/packages/app/src/cli/services/function/build.ts @@ -13,7 +13,7 @@ import {FunctionConfigType} from '../../models/extensions/specifications/functio import {AppInterface} from '../../models/app/app.js' import {EsbuildEnvVarRegex} from '../../constants.js' import {hyphenate, camelize} from '@shopify/cli-kit/common/string' -import {outputContent, outputDebug, outputToken} from '@shopify/cli-kit/node/output' +import {outputDebug} from '@shopify/cli-kit/node/output' import {exec} from '@shopify/cli-kit/node/system' import {dirname, joinPath} from '@shopify/cli-kit/node/path' import {build as esBuild, BuildResult} from 'esbuild' @@ -31,14 +31,22 @@ class InvalidShopifyFunctionPackageError extends AbortError { constructor(message: string) { super( message, - outputContent`Make sure you have a compatible version of the ${outputToken.yellow( - '@shopify/shopify_function', - )} library installed.`, [ - outputContent`Add ${outputToken.green( - `"@shopify/shopify_function": "~${PREFERRED_FUNCTION_NPM_PACKAGE_MAJOR_VERSION}.0.0"`, - )} to the dependencies section of the package.json file in your function's directory, if not already present.` - .value, + {text: 'Make sure you have a compatible version of the '}, + {color: {text: '@shopify/shopify_function', color: 'yellow'}}, + {text: ' library installed.'}, + ], + [ + {text: 'Add '}, + { + color: { + text: `"@shopify/shopify_function": "~${PREFERRED_FUNCTION_NPM_PACKAGE_MAJOR_VERSION}.0.0"`, + color: 'green', + }, + }, + { + text: " to the dependencies section of the package.json file in your function's directory, if not already present.", + }, `Run your package manager's install command to update dependencies.`, ], ) diff --git a/packages/app/src/cli/services/generate-schema.ts b/packages/app/src/cli/services/generate-schema.ts index fd6b92e72eb..51f934d5125 100644 --- a/packages/app/src/cli/services/generate-schema.ts +++ b/packages/app/src/cli/services/generate-schema.ts @@ -5,7 +5,7 @@ import {ExtensionInstance} from '../models/extensions/extension-instance.js' import {FunctionConfigType} from '../models/extensions/specifications/function.js' import {AppLinkedInterface} from '../models/app/app.js' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputInfo, outputResult} from '@shopify/cli-kit/node/output' +import {outputInfo, outputResult, stringifyMessage} from '@shopify/cli-kit/node/output' import {writeFile} from '@shopify/cli-kit/node/fs' import {joinPath} from '@shopify/cli-kit/node/path' @@ -80,8 +80,8 @@ async function generateSchemaFromTarget({ if (!definition) { throw new AbortError( - outputContent`A schema could not be generated for ${localIdentifier}`, - outputContent`Check that the Function targets and version are valid.`, + stringifyMessage(['A schema could not be generated for ', localIdentifier]), + stringifyMessage(['Check that the Function targets and version are valid.']), ) } @@ -109,8 +109,8 @@ async function generateSchemaFromApiType({ if (!definition) { throw new AbortError( - outputContent`A schema could not be generated for ${localIdentifier}`, - outputContent`Check that the Function API type and version are valid.`, + stringifyMessage(['A schema could not be generated for ', localIdentifier]), + stringifyMessage(['Check that the Function API type and version are valid.']), ) } diff --git a/packages/app/src/cli/services/import-extensions.ts b/packages/app/src/cli/services/import-extensions.ts index a702c202a2b..87c993820da 100644 --- a/packages/app/src/cli/services/import-extensions.ts +++ b/packages/app/src/cli/services/import-extensions.ts @@ -9,7 +9,6 @@ import {OrganizationApp} from '../models/organization.js' import {renderSelectPrompt, renderSuccess} from '@shopify/cli-kit/node/ui' import {basename, joinPath} from '@shopify/cli-kit/node/path' import {writeFile} from '@shopify/cli-kit/node/fs' -import {outputContent} from '@shopify/cli-kit/node/output' import {slugify} from '@shopify/cli-kit/common/string' interface ImportOptions { @@ -83,12 +82,10 @@ export async function importExtensions(options: ImportOptions) { } function renderSuccessMessages(generatedExtensions: {extension: ExtensionRegistration; directory: string}[]) { + const extensionLines = generatedExtensions.map((gen) => `• "${gen.extension.title}" at: ${gen.directory}`).join('\n') + renderSuccess({ headline: ['Imported the following extensions from the dashboard:'], - body: generatedExtensions - .map((gen) => { - return outputContent`• "${gen.extension.title}" at: ${gen.directory}`.value - }) - .join('\n'), + body: extensionLines, }) } diff --git a/packages/app/src/cli/services/info.ts b/packages/app/src/cli/services/info.ts index 7df4e0cb570..c76d965b637 100644 --- a/packages/app/src/cli/services/info.ts +++ b/packages/app/src/cli/services/info.ts @@ -9,8 +9,8 @@ import {platformAndArch} from '@shopify/cli-kit/node/os' import {basename, relativePath} from '@shopify/cli-kit/node/path' import { OutputMessage, + TokenizedString, formatPackageManagerCommand, - outputContent, shouldDisplayColors, stringifyMessage, } from '@shopify/cli-kit/node/output' @@ -76,11 +76,10 @@ async function infoApp( }), } } - return outputContent`${JSON.stringify( - Object.fromEntries(Object.entries(appWithSupportedExtensions).filter(([key]) => key !== 'configSchema')), - null, - 2, - )}` + const filteredApp = Object.fromEntries( + Object.entries(appWithSupportedExtensions).filter(([key]) => key !== 'configSchema'), + ) + return new TokenizedString(JSON.stringify(filteredApp, null, 2)) } else { const appInfo = new AppInfo(app, remoteApp, options) return appInfo.output() diff --git a/packages/app/src/cli/services/init/validate.ts b/packages/app/src/cli/services/init/validate.ts index 89d2dcee7ee..d5e48817120 100644 --- a/packages/app/src/cli/services/init/validate.ts +++ b/packages/app/src/cli/services/init/validate.ts @@ -1,7 +1,6 @@ import {isPredefinedTemplate, templates, visibleTemplates} from '../../prompts/init/init.js' import {safeParseURL} from '@shopify/cli-kit/common/url' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputToken} from '@shopify/cli-kit/node/output' export function validateTemplateValue(template: string | undefined) { if (!template) { @@ -15,21 +14,26 @@ export function validateTemplateValue(template: string | undefined) { 'e.g., https://github.com/Shopify//[subpath]#[branch]', ) if (!url && !isPredefinedTemplate(template)) - throw new AbortError( - outputContent`Only ${visibleTemplates - .map((alias) => outputContent`${outputToken.yellow(alias)}`.value) - .join(', ')} template aliases are supported, please provide a valid URL`, - ) + throw new AbortError([ + 'Only ', + ...visibleTemplates.flatMap((alias, index) => [ + ...(index > 0 ? [', '] : []), + {color: {text: alias, color: 'yellow'}}, + ]), + ' template aliases are supported, please provide a valid URL', + ]) } export function validateFlavorValue(template: string | undefined, flavor: string | undefined) { if (!template) { if (flavor) { - throw new AbortError( - outputContent`The ${outputToken.yellow('--flavor')} flag requires the ${outputToken.yellow( - '--template', - )} flag to be set`, - ) + throw new AbortError([ + 'The ', + {color: {text: '--flavor', color: 'yellow'}}, + ' flag requires the ', + {color: {text: '--template', color: 'yellow'}}, + ' flag to be set', + ]) } else { return } @@ -40,24 +44,32 @@ export function validateFlavorValue(template: string | undefined, flavor: string } if (!isPredefinedTemplate(template)) { - throw new AbortError( - outputContent`The ${outputToken.yellow('--flavor')} flag is not supported for custom templates`, - ) + throw new AbortError([ + 'The ', + {color: {text: '--flavor', color: 'yellow'}}, + ' flag is not supported for custom templates', + ]) } const templateConfig = templates[template] if (!templateConfig.branches) { - throw new AbortError(outputContent`The ${outputToken.yellow(template)} template does not support flavors`) + throw new AbortError(['The ', {color: {text: template, color: 'yellow'}}, ' template does not support flavors']) } if (!templateConfig.branches.options[flavor]) { - throw new AbortError( - outputContent`Invalid option for ${outputToken.yellow('--flavor')}\nThe ${outputToken.yellow( - '--flavor', - )} flag for ${outputToken.yellow(template)} accepts only ${Object.keys(templateConfig.branches.options) - .map((alias) => outputContent`${outputToken.yellow(alias)}`.value) - .join(', ')}`, - ) + throw new AbortError([ + 'Invalid option for ', + {color: {text: '--flavor', color: 'yellow'}}, + '\nThe ', + {color: {text: '--flavor', color: 'yellow'}}, + ' flag for ', + {color: {text: template, color: 'yellow'}}, + ' accepts only ', + ...Object.keys(templateConfig.branches.options).flatMap((alias, index) => [ + ...(index > 0 ? [', '] : []), + {color: {text: alias, color: 'yellow'}}, + ]), + ]) } } diff --git a/packages/app/src/cli/services/local-storage.ts b/packages/app/src/cli/services/local-storage.ts index 111eb40d71d..adde4a1770c 100644 --- a/packages/app/src/cli/services/local-storage.ts +++ b/packages/app/src/cli/services/local-storage.ts @@ -1,7 +1,7 @@ import {AppConfigurationFileName} from '../models/app/loader.js' import {LocalStorage} from '@shopify/cli-kit/node/local-storage' -import {outputDebug, outputContent, outputToken} from '@shopify/cli-kit/node/output' import {normalizePath} from '@shopify/cli-kit/node/path' +import {outputDebug} from '@shopify/cli-kit/node/output' export interface CachedAppInfo { directory: string @@ -34,7 +34,7 @@ export function getCachedAppInfo( config: LocalStorage = appLocalStorage(), ): CachedAppInfo | undefined { const normalized = normalizePath(directory) - outputDebug(outputContent`Reading cached app information for directory ${outputToken.path(normalized)}...`) + outputDebug(`Reading cached app information for directory ${normalized}...`) return config.get(normalized) } @@ -43,7 +43,7 @@ export function clearCachedAppInfo( config: LocalStorage = appLocalStorage(), ): void { const normalized = normalizePath(directory) - outputDebug(outputContent`Clearing app information for directory ${outputToken.path(normalized)}...`) + outputDebug(`Clearing app information for directory ${normalized}...`) config.delete(normalized) } @@ -52,11 +52,7 @@ export function setCachedAppInfo( config: LocalStorage = appLocalStorage(), ): void { options.directory = normalizePath(options.directory) - outputDebug( - outputContent`Storing app information for directory ${outputToken.path(options.directory)}:${outputToken.json( - options, - )}`, - ) + outputDebug(`Storing app information for directory ${options.directory}:${JSON.stringify(options)}`) const savedApp = config.get(options.directory) if (savedApp) { config.set(options.directory, { diff --git a/packages/app/src/cli/services/versions-list.ts b/packages/app/src/cli/services/versions-list.ts index c14cd694cf3..b3a2a39fb24 100644 --- a/packages/app/src/cli/services/versions-list.ts +++ b/packages/app/src/cli/services/versions-list.ts @@ -4,7 +4,7 @@ import {AppLinkedInterface} from '../models/app/app.js' import {DeveloperPlatformClient} from '../utilities/developer-platform-client.js' import {Organization, OrganizationApp} from '../models/organization.js' import colors from '@shopify/cli-kit/node/colors' -import {outputContent, outputInfo, outputResult, outputToken, unstyled} from '@shopify/cli-kit/node/output' +import {outputInfo, outputResult, unstyled} from '@shopify/cli-kit/node/output' import {formatDate} from '@shopify/cli-kit/common/string' import {AbortError} from '@shopify/cli-kit/node/error' import {basename} from '@shopify/cli-kit/node/path' @@ -119,10 +119,10 @@ export default async function versionList(options: VersionListOptions) { }, }) - const link = outputToken.link( - developerPlatformClient.webUiName, - [await developerPlatformClient.appDeepLink(remoteApp), 'versions'].join('/'), - ) + const link = await developerPlatformClient.appDeepLink(remoteApp) + const versionsUrl = [link, 'versions'].join('/') - outputInfo(outputContent`\nView all ${String(totalResults)} app versions in the ${link}`) + outputInfo( + `\nView all ${String(totalResults)} app versions in the ${developerPlatformClient.webUiName} ( ${versionsUrl} )`, + ) } diff --git a/packages/app/src/cli/utilities/app/http-reverse-proxy.ts b/packages/app/src/cli/utilities/app/http-reverse-proxy.ts index 0494e045d2e..ccfa8c4434a 100644 --- a/packages/app/src/cli/utilities/app/http-reverse-proxy.ts +++ b/packages/app/src/cli/utilities/app/http-reverse-proxy.ts @@ -1,5 +1,5 @@ import {AbortController} from '@shopify/cli-kit/node/abort' -import {outputDebug, outputContent, outputToken, outputWarn} from '@shopify/cli-kit/node/output' +import {outputDebug, outputWarn} from '@shopify/cli-kit/node/output' import Server from 'http-proxy' import * as http from 'http' import * as https from 'https' @@ -61,10 +61,10 @@ function getProxyServerRequestListener( }) } - outputDebug(outputContent` + outputDebug(` Reverse HTTP proxy error - Invalid path: ${req.url ?? ''} These are the allowed paths: -${outputToken.json(JSON.stringify(rules))} +${JSON.stringify(rules)} `) res.statusCode = 500 diff --git a/packages/app/src/cli/utilities/extensions/fetch-product-variant.ts b/packages/app/src/cli/utilities/extensions/fetch-product-variant.ts index b0901e7f351..792a5539709 100644 --- a/packages/app/src/cli/utilities/extensions/fetch-product-variant.ts +++ b/packages/app/src/cli/utilities/extensions/fetch-product-variant.ts @@ -3,7 +3,6 @@ import {adminRequest} from '@shopify/cli-kit/node/api/admin' import {ensureAuthenticatedAdmin} from '@shopify/cli-kit/node/session' import {AbortError} from '@shopify/cli-kit/node/error' import {normalizeStoreFqdn} from '@shopify/cli-kit/node/context/fqdn' -import {outputContent, outputToken} from '@shopify/cli-kit/node/output' /** * Retrieve the first variant of the first product of the given store @@ -16,15 +15,10 @@ export async function fetchProductVariant(store: string) { const products = result.products.edges if (products.length === 0) { const normalizedUrl = `https://${await normalizeStoreFqdn(store)}/admin/products/new` - const addProductLink = outputContent`${outputToken.link( - 'Add a product', - normalizedUrl, - `You can add a new product here: ${normalizedUrl}`, - )}`.value - throw new AbortError( - 'Could not find a product variant', - `Your store needs to have at least one product to test a 'checkout_ui' extension.\n\n${addProductLink}`, - ) + throw new AbortError('Could not find a product variant', [ + `Your store needs to have at least one product to test a 'checkout_ui' extension.\n\n`, + {link: {label: 'Add a product', url: normalizedUrl}}, + ]) } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion const variantURL = result.products.edges[0]!.node.variants.edges[0]!.node.id diff --git a/packages/app/src/cli/utilities/mkcert.ts b/packages/app/src/cli/utilities/mkcert.ts index 8f18df3e6f1..f648fff274a 100644 --- a/packages/app/src/cli/utilities/mkcert.ts +++ b/packages/app/src/cli/utilities/mkcert.ts @@ -5,14 +5,14 @@ import {downloadGitHubRelease} from '@shopify/cli-kit/node/github' import {fetch, Response} from '@shopify/cli-kit/node/http' import {joinPath, relativePath} from '@shopify/cli-kit/node/path' import {fileExists, readFile, writeFile} from '@shopify/cli-kit/node/fs' -import {outputContent, outputDebug, outputInfo, outputToken} from '@shopify/cli-kit/node/output' +import {outputDebug, outputInfo} from '@shopify/cli-kit/node/output' import {AbortError, BugError} from '@shopify/cli-kit/node/error' import which from 'which' import {RenderAlertOptions, keypress, renderInfo, renderTasks, renderWarning} from '@shopify/cli-kit/node/ui' const MKCERT_VERSION = 'v1.4.4' const MKCERT_REPO = 'FiloSottile/mkcert' -const mkcertSnippet = outputToken.genericShellCommand('mkcert') +const mkcertSnippet = 'mkcert' /** * Gets the path to the mkcert binary. @@ -41,7 +41,7 @@ async function getMkcertPath( const mkcertLocation = await which('mkcert', {nothrow: true}) if (mkcertLocation) { - outputDebug(outputContent`Found ${mkcertSnippet} at ${outputToken.path(mkcertLocation)}`) + outputDebug(`Found ${mkcertSnippet} at ${mkcertLocation}`) return mkcertLocation } @@ -74,7 +74,7 @@ async function downloadMkcert(targetPath: string, platform: NodeJS.Platform, arc await downloadGitHubRelease(MKCERT_REPO, MKCERT_VERSION, assetName, targetPath) - outputDebug(outputContent`${mkcertSnippet} saved to ${outputToken.path(targetPath)}`) + outputDebug(`${mkcertSnippet} saved to ${targetPath}`) } /** @@ -172,7 +172,7 @@ export async function generateCertificate({ mkcertPath = await getMkcertPath(dotShopifyPath, env, platform, arch) licenseError = await downloadMkcertLicense(dotShopifyPath) - outputDebug(outputContent`${mkcertSnippet} found at: ${outputToken.path(mkcertPath)}`) + outputDebug(`${mkcertSnippet} found at: ${mkcertPath}`) }, }, ]) @@ -181,9 +181,9 @@ export async function generateCertificate({ renderInfo(licenseError) } - outputInfo(outputContent`Generating self-signed certificate for localhost. You may be prompted for your password.`) + outputInfo('Generating self-signed certificate for localhost. You may be prompted for your password.') await exec(mkcertPath, ['-install', '-key-file', keyPath, '-cert-file', certPath, 'localhost']) - outputInfo(outputContent`${outputToken.successIcon()} Certificate generated at ${relativeCertPath}\n`) + outputInfo(`✓ Certificate generated at ${relativeCertPath}\n`) const wsl = await isWsl() if (wsl) { diff --git a/packages/app/src/cli/validations/version-name.ts b/packages/app/src/cli/validations/version-name.ts index 1b4f1fd7b77..1ca81975311 100644 --- a/packages/app/src/cli/validations/version-name.ts +++ b/packages/app/src/cli/validations/version-name.ts @@ -3,7 +3,7 @@ import {AbortError} from '@shopify/cli-kit/node/error' export function validateVersion(version: string | undefined) { if (typeof version === 'undefined') return - const errorMessage = ['Invalid version name:', {userInput: version}] + const errorMessage = ['Invalid version name: ', {userInput: version}] const versionMaxLength = 100 if (version.length > versionMaxLength) { diff --git a/packages/cli-kit/src/private/node/api/graphql.ts b/packages/cli-kit/src/private/node/api/graphql.ts index 9a0136db2c6..8e0be3c9046 100644 --- a/packages/cli-kit/src/private/node/api/graphql.ts +++ b/packages/cli-kit/src/private/node/api/graphql.ts @@ -1,6 +1,6 @@ import {GraphQLClientError, sanitizedHeadersOutput} from './headers.js' import {sanitizeURL} from './urls.js' -import {stringifyMessage, outputContent, outputToken, outputDebug} from '../../../public/node/output.js' +import {outputDebug} from '../../../public/node/output.js' import {AbortError} from '../../../public/node/error.js' import {ClientError, Variables} from 'graphql-request' @@ -11,8 +11,8 @@ export function debugLogRequestInfo( variables?: Variables, headers: {[key: string]: string} = {}, ) { - outputDebug(outputContent`Sending ${outputToken.json(api)} GraphQL request: - ${outputToken.raw(query.toString().trim())} + outputDebug(`Sending ${api} GraphQL request: + ${query.toString().trim()} ${variables ? `\nWith variables:\n${sanitizeVariables(variables)}\n` : ''} With request headers: ${sanitizedHeadersOutput(headers)}\n @@ -69,13 +69,11 @@ export function errorHandler(api: string): (error: unknown, requestId?: string) return (error: unknown, requestId?: string) => { if (error instanceof ClientError) { const {status} = error.response - let errorMessage = stringifyMessage(outputContent` -The ${outputToken.raw(api)} GraphQL API responded unsuccessfully with${ - status === 200 ? '' : ` the HTTP status ${status} and` - } errors: + let errorMessage = ` +The ${api} GraphQL API responded unsuccessfully with${status === 200 ? '' : ` the HTTP status ${status} and`} errors: -${outputToken.json(error.response.errors)} - `) +${JSON.stringify(error.response.errors, null, 2)} + ` if (requestId) { errorMessage += ` Request ID: ${requestId} diff --git a/packages/cli-kit/src/private/node/conf-store.ts b/packages/cli-kit/src/private/node/conf-store.ts index 49581a5f306..3c19d6c8b7e 100644 --- a/packages/cli-kit/src/private/node/conf-store.ts +++ b/packages/cli-kit/src/private/node/conf-store.ts @@ -1,6 +1,6 @@ import {isUnitTest} from '../../public/node/context/local.js' import {LocalStorage} from '../../public/node/local-storage.js' -import {outputContent, outputDebug} from '@shopify/cli-kit/node/output' +import {outputDebug} from '@shopify/cli-kit/node/output' interface CacheValue { value: T @@ -50,7 +50,7 @@ function cliKitStore() { * @returns Session. */ export function getSession(config: LocalStorage = cliKitStore()): string | undefined { - outputDebug(outputContent`Getting session store...`) + outputDebug('Getting session store...') return config.get('sessionStore') } @@ -60,7 +60,7 @@ export function getSession(config: LocalStorage = cliKitStore()): st * @param session - Session. */ export function setSession(session: string, config: LocalStorage = cliKitStore()): void { - outputDebug(outputContent`Setting session store...`) + outputDebug('Setting session store...') config.set('sessionStore', session) } @@ -68,7 +68,7 @@ export function setSession(session: string, config: LocalStorage = c * Remove session. */ export function removeSession(config: LocalStorage = cliKitStore()): void { - outputDebug(outputContent`Removing session store...`) + outputDebug('Removing session store...') config.delete('sessionStore') } diff --git a/packages/cli-kit/src/private/node/otel-metrics.ts b/packages/cli-kit/src/private/node/otel-metrics.ts index dae983de673..653630a3ac0 100644 --- a/packages/cli-kit/src/private/node/otel-metrics.ts +++ b/packages/cli-kit/src/private/node/otel-metrics.ts @@ -1,5 +1,5 @@ import {MetricInstrumentType, OtelService} from '../../public/node/vendor/otel-js/service/types.js' -import {outputContent, outputDebug, outputToken} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import { DefaultOtelService, DefaultOtelServiceOptions, @@ -155,7 +155,7 @@ function globalOtelService(options: CreateMetricRecorderOptions): OtelService { */ function recordCommandCounter(recorder: MetricRecorder, labels: Labels) { if (recorder === 'console') { - outputDebug(outputContent`[OTEL] record ${Name.Counter} counter ${outputToken.json({labels})}`) + outputDebug(`[OTEL] record ${Name.Counter} counter ${JSON.stringify({labels}, null, 2)}`) return } recorder.otel.record(Name.Counter, 1, labels) @@ -167,13 +167,17 @@ function recordCommandCounter(recorder: MetricRecorder, labels: Labels) { function recordCommandTiming(recorder: MetricRecorder, labels: Labels, timing: Timing) { if (recorder === 'console') { outputDebug( - outputContent`[OTEL] record ${Name.Duration} histogram ${timing.active.toString()}ms ${outputToken.json({ - labels, - })}`, + `[OTEL] record ${Name.Duration} histogram ${timing.active.toString()}ms ${JSON.stringify( + { + labels, + }, + null, + 2, + )}`, ) - outputDebug(outputContent`[OTEL] record ${Name.Elapsed} histogram stage="active" ${timing.active.toString()}ms`) - outputDebug(outputContent`[OTEL] record ${Name.Elapsed} histogram stage="network" ${timing.network.toString()}ms`) - outputDebug(outputContent`[OTEL] record ${Name.Elapsed} histogram stage="prompt" ${timing.prompt.toString()}ms`) + outputDebug(`[OTEL] record ${Name.Elapsed} histogram stage="active" ${timing.active.toString()}ms`) + outputDebug(`[OTEL] record ${Name.Elapsed} histogram stage="network" ${timing.network.toString()}ms`) + outputDebug(`[OTEL] record ${Name.Elapsed} histogram stage="prompt" ${timing.prompt.toString()}ms`) return } diff --git a/packages/cli-kit/src/private/node/session.ts b/packages/cli-kit/src/private/node/session.ts index a6e620ad8a8..ffc5e68f862 100644 --- a/packages/cli-kit/src/private/node/session.ts +++ b/packages/cli-kit/src/private/node/session.ts @@ -13,7 +13,7 @@ import {IdentityToken, Session} from './session/schema.js' import * as secureStore from './session/store.js' import {pollForDeviceAuthorization, requestDeviceAuthorization} from './session/device-authorization.js' import {isThemeAccessSession} from './api/rest.js' -import {outputContent, outputToken, outputDebug} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import {firstPartyDev, themeToken} from '../../public/node/context/local.js' import {AbortError, BugError} from '../../public/node/error.js' import {normalizeStoreFqdn, identityFqdn} from '../../public/node/context/fqdn.js' @@ -190,10 +190,10 @@ export async function ensureAuthenticated( const fqdnSession = currentSession[fqdn]! const scopes = getFlattenScopes(applications) - outputDebug(outputContent`Validating existing session against the scopes: -${outputToken.json(scopes)} + outputDebug(`Validating existing session against the scopes: +${JSON.stringify(scopes, null, 2)} For applications: -${outputToken.json(applications)} +${JSON.stringify(applications, null, 2)} `) const validationResult = await validateSession(scopes, applications, fqdnSession) @@ -211,10 +211,10 @@ The CLI is currently unable to prompt for reauthentication.`, if (validationResult === 'needs_full_auth') { throwOnNoPrompt() - outputDebug(outputContent`Initiating the full authentication flow...`) + outputDebug('Initiating the full authentication flow...') newSession = await executeCompleteFlow(applications, fqdn) } else if (validationResult === 'needs_refresh' || forceRefresh) { - outputDebug(outputContent`The current session is valid but needs refresh. Refreshing...`) + outputDebug('The current session is valid but needs refresh. Refreshing...') try { newSession = await refreshTokens(fqdnSession.identity, applications, fqdn) } catch (error) { @@ -258,7 +258,7 @@ async function executeCompleteFlow(applications: OAuthApplications, identityFqdn const exchangeScopes = getExchangeScopes(applications) const store = applications.adminApi?.storeFqdn if (firstPartyDev()) { - outputDebug(outputContent`Authenticating as Shopify Employee...`) + outputDebug('Authenticating as Shopify Employee...') scopes.push('employee') } @@ -268,16 +268,16 @@ async function executeCompleteFlow(applications: OAuthApplications, identityFqdn identityToken = buildIdentityTokenFromEnv(scopes, identityTokenInformation) } else { // Request a device code to authorize without a browser redirect. - outputDebug(outputContent`Requesting device authorization code...`) + outputDebug('Requesting device authorization code...') const deviceAuth = await requestDeviceAuthorization(scopes) // Poll for the identity token - outputDebug(outputContent`Starting polling for the identity token...`) + outputDebug('Starting polling for the identity token...') identityToken = await pollForDeviceAuthorization(deviceAuth.deviceCode, deviceAuth.interval) } // Exchange identity token for application tokens - outputDebug(outputContent`CLI token received. Exchanging it for application tokens...`) + outputDebug('CLI token received. Exchanging it for application tokens...') const result = await exchangeAccessForApplicationTokens(identityToken, exchangeScopes, store) const session: Session = { diff --git a/packages/cli-kit/src/private/node/session/device-authorization.ts b/packages/cli-kit/src/private/node/session/device-authorization.ts index 7179b105085..3c506967bac 100644 --- a/packages/cli-kit/src/private/node/session/device-authorization.ts +++ b/packages/cli-kit/src/private/node/session/device-authorization.ts @@ -3,7 +3,7 @@ import {exchangeDeviceCodeForAccessToken} from './exchange.js' import {IdentityToken} from './schema.js' import {identityFqdn} from '../../../public/node/context/fqdn.js' import {shopifyFetch} from '../../../public/node/http.js' -import {outputContent, outputDebug, outputInfo, outputToken} from '../../../public/node/output.js' +import {outputDebug, outputInfo} from '../../../public/node/output.js' import {AbortError, BugError} from '../../../public/node/error.js' import {isCloudEnvironment} from '../../../public/node/context/local.js' import {isCI, openURL} from '../../../public/node/system.js' @@ -43,7 +43,7 @@ export async function requestDeviceAuthorization(scopes: string[]): Promise { - outputInfo(outputContent`👉 Open this link to start the auth process: ${linkToken}`) + outputInfo(`👉 Open this link to start the auth process: ${verificationLink}`) } if (isCloudEnvironment() || !isTTY()) { @@ -71,7 +71,7 @@ export async function requestDeviceAuthorization(scopes: string[]): Promise { + test('renders green text correctly', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"Success!"`) + }) + + test('renders yellow text correctly', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"Warning!"`) + }) + + test('renders cyan text correctly', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"Info!"`) + }) + + test('renders magenta text correctly', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"Highlight!"`) + }) + + test('renders gray text correctly', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"Subdued text"`) + }) +}) diff --git a/packages/cli-kit/src/private/node/ui/components/ColoredText.tsx b/packages/cli-kit/src/private/node/ui/components/ColoredText.tsx new file mode 100644 index 00000000000..62c2ad930d4 --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/components/ColoredText.tsx @@ -0,0 +1,19 @@ +import {Text} from 'ink' +import React, {FunctionComponent} from 'react' + +type InkColor = 'green' | 'yellow' | 'cyan' | 'magenta' | 'gray' | 'blue' | 'red' | 'white' | 'black' + +interface ColoredTextProps { + text: string + color: InkColor +} + +/** + * `ColoredText` displays text in the specified color. + */ +const ColoredText: FunctionComponent = ({text, color}): JSX.Element => { + return {text} +} + +export {ColoredText} +export type {InkColor} diff --git a/packages/cli-kit/src/private/node/ui/components/DebugMessage.test.tsx b/packages/cli-kit/src/private/node/ui/components/DebugMessage.test.tsx new file mode 100644 index 00000000000..7c93982c36c --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/components/DebugMessage.test.tsx @@ -0,0 +1,18 @@ +import {DebugMessage} from './DebugMessage.js' +import {render} from '../../testing/ui.js' +import {describe, expect, test} from 'vitest' +import React from 'react' + +describe('DebugMessage', async () => { + test('renders correctly with debug prefix', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"[DEBUG] Test debug message"`) + }) + + test('handles empty message', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"[DEBUG] "`) + }) +}) diff --git a/packages/cli-kit/src/private/node/ui/components/DebugMessage.tsx b/packages/cli-kit/src/private/node/ui/components/DebugMessage.tsx new file mode 100644 index 00000000000..06378579dc8 --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/components/DebugMessage.tsx @@ -0,0 +1,15 @@ +import {Text} from 'ink' +import React, {FunctionComponent} from 'react' + +interface DebugMessageProps { + message: string +} + +/** + * `DebugMessage` displays debug messages in a subdued, dimmed format. + */ +const DebugMessage: FunctionComponent = ({message}): JSX.Element => { + return [DEBUG] {message} +} + +export {DebugMessage} diff --git a/packages/cli-kit/src/private/node/ui/components/Icon.test.tsx b/packages/cli-kit/src/private/node/ui/components/Icon.test.tsx new file mode 100644 index 00000000000..499d464c5ac --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/components/Icon.test.tsx @@ -0,0 +1,30 @@ +import {Icon} from './Icon.js' +import {render} from '../../testing/ui.js' +import {describe, expect, test} from 'vitest' +import React from 'react' + +describe('Icon', async () => { + test('renders success icon correctly', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"✓"`) + }) + + test('renders fail icon correctly', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"✗"`) + }) + + test('renders warning icon correctly', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"⚠"`) + }) + + test('renders info icon correctly', async () => { + const {lastFrame} = render() + + expect(lastFrame()).toMatchInlineSnapshot(`"ℹ"`) + }) +}) diff --git a/packages/cli-kit/src/private/node/ui/components/Icon.tsx b/packages/cli-kit/src/private/node/ui/components/Icon.tsx new file mode 100644 index 00000000000..f16a1c71ea1 --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/components/Icon.tsx @@ -0,0 +1,24 @@ +import {Text} from 'ink' +import React, {FunctionComponent} from 'react' + +interface IconProps { + type: 'success' | 'fail' | 'warning' | 'info' +} + +/** + * `Icon` displays common status icons. + */ +const Icon: FunctionComponent = ({type}): JSX.Element => { + const iconMap = { + success: {symbol: '✓', color: 'green' as const}, + fail: {symbol: '✗', color: 'red' as const}, + warning: {symbol: '⚠', color: 'yellow' as const}, + info: {symbol: 'ℹ', color: 'blue' as const}, + } + + const {symbol, color} = iconMap[type] + + return {symbol} +} + +export {Icon} diff --git a/packages/cli-kit/src/private/node/ui/components/JsonDisplay.test.tsx b/packages/cli-kit/src/private/node/ui/components/JsonDisplay.test.tsx new file mode 100644 index 00000000000..56845c51b85 --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/components/JsonDisplay.test.tsx @@ -0,0 +1,28 @@ +import {JsonDisplay} from './JsonDisplay.js' +import {render} from '../../testing/ui.js' +import {describe, expect, test} from 'vitest' +import React from 'react' + +describe('JsonDisplay', async () => { + test('renders simple object correctly', async () => { + const data = {name: 'test', value: 42} + const {lastFrame} = render() + + expect(lastFrame()).toContain('test') + expect(lastFrame()).toContain('42') + }) + + test('renders array correctly', async () => { + const data = ['item1', 'item2', 'item3'] + const {lastFrame} = render() + + expect(lastFrame()).toContain('item1') + expect(lastFrame()).toContain('item2') + expect(lastFrame()).toContain('item3') + }) + + test('handles null and undefined gracefully', async () => { + const {lastFrame} = render() + expect(lastFrame()).toContain('null') + }) +}) diff --git a/packages/cli-kit/src/private/node/ui/components/JsonDisplay.tsx b/packages/cli-kit/src/private/node/ui/components/JsonDisplay.tsx new file mode 100644 index 00000000000..26f34850a98 --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/components/JsonDisplay.tsx @@ -0,0 +1,25 @@ +import {Text} from 'ink' +import React, {FunctionComponent} from 'react' +import cjs from 'color-json' + +interface JsonDisplayProps { + data: unknown +} + +/** + * `JsonDisplay` displays JSON data with syntax highlighting. + */ +const JsonDisplay: FunctionComponent = ({data}): JSX.Element => { + try { + const coloredJson = cjs(data) + return {coloredJson} + } catch (error) { + if (error instanceof Error) { + const fallbackJson = JSON.stringify(data, null, 2) + return {fallbackJson} + } + throw error + } +} + +export {JsonDisplay} diff --git a/packages/cli-kit/src/private/node/ui/components/TokenizedText.test.tsx b/packages/cli-kit/src/private/node/ui/components/TokenizedText.test.tsx index f0dfefc46e0..6e9c3f3297d 100644 --- a/packages/cli-kit/src/private/node/ui/components/TokenizedText.test.tsx +++ b/packages/cli-kit/src/private/node/ui/components/TokenizedText.test.tsx @@ -43,6 +43,21 @@ describe('TokenizedText', async () => { { error: 'some error', }, + { + color: { + text: 'green text', + color: 'green', + }, + }, + { + json: {key: 'value'}, + }, + { + icon: 'success', + }, + { + debug: 'debug info', + }, ] const {lastFrame} = render() @@ -52,7 +67,9 @@ describe('TokenizedText', async () => { • Item 1 • Item 2 • Item 3 - src/this/is/a/test.js some info some warn some error" + src/this/is/a/test.js some info some warn some error green text { + \\"key\\": \\"value\\" + } ✓ [DEBUG] debug info" `) }) @@ -74,4 +91,44 @@ describe('TokenizedText', async () => { ).toBe('Run Item 1 Item 2 Item 3') }) }) + + test('renders colored text token correctly', async () => { + const item = { + color: { + text: 'Success message', + color: 'green' as const, + }, + } + + const {lastFrame} = render() + expect(lastFrame()).toContain('Success message') + }) + + test('renders json token correctly', async () => { + const item = { + json: {name: 'test', value: 42}, + } + + const {lastFrame} = render() + expect(lastFrame()).toContain('test') + expect(lastFrame()).toContain('42') + }) + + test('renders icon token correctly', async () => { + const item = { + icon: 'success' as const, + } + + const {lastFrame} = render() + expect(lastFrame()).toContain('✓') + }) + + test('renders debug token correctly', async () => { + const item = { + debug: 'debug information', + } + + const {lastFrame} = render() + expect(lastFrame()).toContain('[DEBUG] debug information') + }) }) diff --git a/packages/cli-kit/src/private/node/ui/components/TokenizedText.tsx b/packages/cli-kit/src/private/node/ui/components/TokenizedText.tsx index 66a7b1928d3..ae159f32d8e 100644 --- a/packages/cli-kit/src/private/node/ui/components/TokenizedText.tsx +++ b/packages/cli-kit/src/private/node/ui/components/TokenizedText.tsx @@ -5,6 +5,12 @@ import {List} from './List.js' import {UserInput} from './UserInput.js' import {FilePath} from './FilePath.js' import {Subdued} from './Subdued.js' +import {ColoredText, InkColor} from './ColoredText.js' +import {JsonDisplay} from './JsonDisplay.js' +import {Icon} from './Icon.js' +import {DebugMessage} from './DebugMessage.js' +import {shouldDisplayColors} from '../../../../public/node/output.js' +import colors from '../../../../public/node/colors.js' import {Box, Text} from 'ink' import React, {FunctionComponent} from 'react' @@ -31,6 +37,25 @@ export interface BoldToken { bold: string } +export interface ColorToken { + color: { + text: string + color: InkColor + } +} + +export interface JsonToken { + json: unknown +} + +export interface IconToken { + icon: 'success' | 'fail' | 'warning' | 'info' +} + +export interface DebugToken { + debug: string +} + export type Token = | string | { @@ -58,6 +83,10 @@ export type Token = | { error: string } + | ColorToken + | JsonToken + | IconToken + | DebugToken export type InlineToken = Exclude export type TokenItem = T | T[] @@ -75,6 +104,31 @@ function tokenToBlock(token: Token): Block { } } +function getColorFunction(color: InkColor): ((text: string) => string) | null { + switch (color) { + case 'cyan': + return colors.cyan + case 'yellow': + return colors.yellow + case 'red': + return colors.red + case 'green': + return colors.green + case 'blue': + return colors.blue + case 'magenta': + return colors.magenta + case 'gray': + return colors.gray + case 'white': + return colors.white + case 'black': + return colors.black + default: + return null + } +} + export function tokenItemToString(token: TokenItem): string { if (typeof token === 'string') { return token @@ -100,14 +154,51 @@ export function tokenItemToString(token: TokenItem): string { return token.warn } else if ('error' in token) { return token.error + } else if ('color' in token) { + if (shouldDisplayColors()) { + const colorFunction = getColorFunction(token.color.color) + return colorFunction ? colorFunction(token.color.text) : token.color.text + } + return token.color.text + } else if ('json' in token) { + return JSON.stringify(token.json) + } else if ('icon' in token) { + const iconMap = {success: '✓', fail: '✗', warning: '⚠', info: 'ℹ'} + return iconMap[token.icon] + } else if ('debug' in token) { + return `[DEBUG] ${token.debug}` } else { return token .map((item, index) => { - if (index !== 0 && !(typeof item !== 'string' && 'char' in item)) { - return ` ${tokenItemToString(item)}` - } else { + if (index === 0) { + return tokenItemToString(item) + } + + const prevItem = token[index - 1] + const prevItemString = tokenItemToString(prevItem) + + // Don't add space if current item is a char token (punctuation) + if (typeof item !== 'string' && 'char' in item) { + return tokenItemToString(item) + } + + // Don't add space if previous item ends with whitespace + if (prevItemString.endsWith(' ') || prevItemString.endsWith('\n') || prevItemString.endsWith('\t')) { return tokenItemToString(item) } + + // Don't add space if current item is a whitespace string or starts with punctuation + if (typeof item === 'string' && item.match(/^\s/)) { + return tokenItemToString(item) + } + + const currentItemString = tokenItemToString(item) + if (currentItemString.match(/^[.,;:!?]/)) { + return currentItemString + } + + // Add space for normal word boundaries + return ` ${tokenItemToString(item)}` }) .join('') } @@ -134,12 +225,48 @@ function splitByDisplayType(acc: Block[][], item: Block) { const InlineBlocks: React.FC<{blocks: Block[]}> = ({blocks}) => { return ( - {blocks.map((block, blockIndex) => ( - - {blockIndex !== 0 && !(typeof block.value !== 'string' && 'char' in block.value) && } - - - ))} + {blocks.map((block, blockIndex) => { + if (blockIndex === 0) { + return ( + + + + ) + } + + const prevBlock = blocks[blockIndex - 1] + const prevBlockString = tokenItemToString(prevBlock!.value) + + // Determine if we should add a space + let shouldAddSpace = true + + // Don't add space if current item is a char token (punctuation) + if (typeof block.value !== 'string' && 'char' in block.value) { + shouldAddSpace = false + } + + // Don't add space if previous item ends with whitespace + if (prevBlockString.endsWith(' ') || prevBlockString.endsWith('\n') || prevBlockString.endsWith('\t')) { + shouldAddSpace = false + } + + // Don't add space if current item is a whitespace string or starts with punctuation + if (typeof block.value === 'string' && block.value.match(/^\s/)) { + shouldAddSpace = false + } + + const currentBlockString = tokenItemToString(block.value) + if (currentBlockString.match(/^[.,;:!?]/)) { + shouldAddSpace = false + } + + return ( + + {shouldAddSpace && } + + + ) + })} ) } @@ -177,6 +304,14 @@ const TokenizedText: FunctionComponent = ({item}) => { return {item.warn} } else if ('error' in item) { return {item.error} + } else if ('color' in item) { + return + } else if ('json' in item) { + return + } else if ('icon' in item) { + return + } else if ('debug' in item) { + return } else { const groupedItems = item.map(tokenToBlock).reduce(splitByDisplayType, []) diff --git a/packages/cli-kit/src/private/node/ui/migration-helpers.test.tsx b/packages/cli-kit/src/private/node/ui/migration-helpers.test.tsx new file mode 100644 index 00000000000..1aed229f79b --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/migration-helpers.test.tsx @@ -0,0 +1,93 @@ +import { + createColorToken, + createJsonToken, + createIconToken, + createDebugToken, + MigratedText, + GreenText, + YellowText, + CyanText, + MagentaText, + GrayText, + SuccessIcon, + FailIcon, +} from './migration-helpers.js' +import {render} from '../testing/ui.js' +import {describe, expect, test} from 'vitest' +import React from 'react' + +describe('Migration Helpers', async () => { + test('createColorToken creates valid color tokens', () => { + const token = createColorToken('SUCCESS', 'green') + expect(token).toEqual({ + color: { + text: 'SUCCESS', + color: 'green', + }, + }) + }) + + test('createJsonToken creates valid json tokens', () => { + const data = {key: 'value', number: 42} + const token = createJsonToken(data) + expect(token).toEqual({ + json: data, + }) + }) + + test('createIconToken creates valid icon tokens', () => { + const token = createIconToken('success') + expect(token).toEqual({ + icon: 'success', + }) + }) + + test('createDebugToken creates valid debug tokens', () => { + const token = createDebugToken('debug message') + expect(token).toEqual({ + debug: 'debug message', + }) + }) + + test('MigratedText renders complex content correctly', async () => { + const content = [ + createColorToken('SUCCESS', 'green'), + ' App deployed to ', + {filePath: '/apps/my-app'}, + ' Run ', + {command: 'npm start'}, + ] + + const {lastFrame} = render() + + expect(lastFrame()).toContain('SUCCESS') + expect(lastFrame()).toContain('App deployed to') + expect(lastFrame()).toContain('/apps/my-app') + expect(lastFrame()).toContain('npm start') + }) + + test('Direct color components render correctly', async () => { + const {lastFrame: green} = render(Success!) + expect(green()).toMatchInlineSnapshot(`"Success!"`) + + const {lastFrame: yellow} = render(Warning!) + expect(yellow()).toMatchInlineSnapshot(`"Warning!"`) + + const {lastFrame: cyan} = render(Info!) + expect(cyan()).toMatchInlineSnapshot(`"Info!"`) + + const {lastFrame: magenta} = render(Highlight!) + expect(magenta()).toMatchInlineSnapshot(`"Highlight!"`) + + const {lastFrame: gray} = render(Subdued!) + expect(gray()).toMatchInlineSnapshot(`"Subdued!"`) + }) + + test('Icon shortcuts render correctly', async () => { + const {lastFrame: success} = render() + expect(success()).toMatchInlineSnapshot(`"✓"`) + + const {lastFrame: fail} = render() + expect(fail()).toMatchInlineSnapshot(`"✗"`) + }) +}) diff --git a/packages/cli-kit/src/private/node/ui/migration-helpers.tsx b/packages/cli-kit/src/private/node/ui/migration-helpers.tsx new file mode 100644 index 00000000000..93cdd00eb39 --- /dev/null +++ b/packages/cli-kit/src/private/node/ui/migration-helpers.tsx @@ -0,0 +1,117 @@ +import {ColoredText} from './components/ColoredText.js' +import {Icon} from './components/Icon.js' +import {TokenizedText, TokenItem} from './components/TokenizedText.js' +import React from 'react' + +/** + * Migration utilities to help convert outputContent + outputToken patterns + * to the new ink.js React component system. + */ + +/** + * Helper to create a colored text token for use in TokenizedText + */ +export function createColorToken( + text: string, + color: 'green' | 'yellow' | 'cyan' | 'magenta' | 'gray' | 'blue' | 'red', +) { + return { + color: { + text, + color, + }, + } +} + +/** + * Helper to create a JSON token for use in TokenizedText + */ +export function createJsonToken(data: unknown) { + return { + json: data, + } +} + +/** + * Helper to create an icon token for use in TokenizedText + */ +export function createIconToken(type: 'success' | 'fail' | 'warning' | 'info') { + return { + icon: type, + } +} + +/** + * Helper to create a debug token for use in TokenizedText + */ +export function createDebugToken(message: string) { + return { + debug: message, + } +} + +/** + * Quick migration wrapper for simple outputContent patterns. + * Converts an array of mixed content to a TokenizedText component. + * + * @example + * ``` + * // Old: + * outputContent`$\{outputToken.green('SUCCESS')\} App deployed to $\{outputToken.path('/apps/my-app')\}` + * + * // New: + * \ + * ``` + */ +export const MigratedText: React.FC<{content: TokenItem}> = ({content}) => { + return +} + +/** + * Direct component alternatives for standalone usage + */ + +// Direct color component alternatives +export const GreenText: React.FC<{children: string}> = ({children}) => + +export const YellowText: React.FC<{children: string}> = ({children}) => + +export const CyanText: React.FC<{children: string}> = ({children}) => + +export const MagentaText: React.FC<{children: string}> = ({children}) => + +export const GrayText: React.FC<{children: string}> = ({children}) => + +// Icon shortcuts +export const SuccessIcon: React.FC = () => +export const FailIcon: React.FC = () => + +/** + * Migration examples and common patterns + */ +export const migrationExamples = { + // outputToken.green('SUCCESS') -> createColorToken('SUCCESS', 'green') + coloredText: createColorToken('SUCCESS', 'green'), + + // outputToken.path('/path/to/file') -> {filePath: '/path/to/file'} + filePath: {filePath: '/path/to/file'}, + + // outputToken.genericShellCommand('npm install') -> {command: 'npm install'} + command: {command: 'npm install'}, + + // outputToken.json(data) -> createJsonToken(data) + json: createJsonToken({key: 'value'}), + + // outputToken.successIcon() -> createIconToken('success') + successIcon: createIconToken('success'), + + // outputToken.failIcon() -> createIconToken('fail') + failIcon: createIconToken('fail'), + + // Debug messages -> createDebugToken('message') + debug: createDebugToken('debug information'), +} diff --git a/packages/cli-kit/src/public/node/analytics.ts b/packages/cli-kit/src/public/node/analytics.ts index 5fc253c88d0..91279d52919 100644 --- a/packages/cli-kit/src/public/node/analytics.ts +++ b/packages/cli-kit/src/public/node/analytics.ts @@ -3,7 +3,7 @@ import * as metadata from './metadata.js' import {publishMonorailEvent, MONORAIL_COMMAND_TOPIC} from './monorail.js' import {fanoutHooks} from './plugins.js' -import {outputContent, outputDebug, outputToken} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import {getEnvironmentData, getSensitiveEnvironmentData} from '../../private/node/analytics.js' import {CLI_KIT_VERSION} from '../common/version.js' import {recordMetrics} from '../../private/node/otel-metrics.js' @@ -50,14 +50,14 @@ export async function reportAnalyticsEvent(options: ReportAnalyticsEventOptions) }, }) if (!withinRateLimit) { - outputDebug(outputContent`Skipping command analytics due to rate limiting, payload: ${outputToken.json(payload)}`) + outputDebug(`Skipping command analytics due to rate limiting, payload: ${JSON.stringify(payload, null, 2)}`) return } const skipMonorailAnalytics = !alwaysLogAnalytics() && analyticsDisabled() const skipMetricAnalytics = !alwaysLogMetrics() && analyticsDisabled() if (skipMonorailAnalytics || skipMetricAnalytics) { - outputDebug(outputContent`Skipping command analytics, payload: ${outputToken.json(payload)}`) + outputDebug(`Skipping command analytics, payload: ${JSON.stringify(payload, null, 2)}`) } const doMonorail = async () => { diff --git a/packages/cli-kit/src/public/node/api/admin.ts b/packages/cli-kit/src/public/node/api/admin.ts index 2aeebeb5649..d69f234e454 100644 --- a/packages/cli-kit/src/public/node/api/admin.ts +++ b/packages/cli-kit/src/public/node/api/admin.ts @@ -1,6 +1,6 @@ import {graphqlRequest, graphqlRequestDoc, GraphQLResponseOptions, GraphQLVariables} from './graphql.js' import {AdminSession} from '../session.js' -import {outputContent, outputToken} from '../../../public/node/output.js' +import {stringifyMessage} from '../../../public/node/output.js' import {AbortError, BugError} from '../error.js' import { restRequestBody, @@ -141,11 +141,12 @@ async function fetchApiVersions(session: AdminSession): Promise { if (error instanceof ClientError && error.response.status === 403) { const storeName = session.storeFqdn.replace('.myshopify.com', '') throw new AbortError( - outputContent`Looks like you don't have access this dev store: (${outputToken.link( - storeName, - `https://${session.storeFqdn}`, - )})`, - outputContent`If you're not the owner, create a dev store staff account for yourself`, + stringifyMessage([ + "Looks like you don't have access this dev store: (", + {link: {label: storeName, url: `https://${session.storeFqdn}`}}, + ')', + ]), + "If you're not the owner, create a dev store staff account for yourself", ) } if (error instanceof ClientError && (error.response.status === 401 || error.response.status === 404)) { diff --git a/packages/cli-kit/src/public/node/archiver.ts b/packages/cli-kit/src/public/node/archiver.ts index 9d0984afce9..cd523637709 100644 --- a/packages/cli-kit/src/public/node/archiver.ts +++ b/packages/cli-kit/src/public/node/archiver.ts @@ -1,6 +1,6 @@ import {relativePath} from './path.js' import {glob, removeFile} from './fs.js' -import {outputDebug, outputContent, outputToken} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import archiver from 'archiver' import {joinPath} from '@shopify/cli-kit/node/path' import {createWriteStream, readFileSync, writeFileSync} from 'fs' @@ -33,7 +33,7 @@ interface ZipOptions { */ export async function zip(options: ZipOptions): Promise { const {inputDirectory, outputZipPath, matchFilePattern = '**/*'} = options - outputDebug(outputContent`Zipping ${outputToken.path(inputDirectory)} into ${outputToken.path(outputZipPath)}`) + outputDebug(`Zipping ${inputDirectory} into ${outputZipPath}`) const pathsToZip = await glob(matchFilePattern, { cwd: inputDirectory, absolute: true, @@ -154,7 +154,7 @@ export async function brotliCompress(options: BrotliOptions): Promise { await removeFile(tempTarPath) // eslint-disable-next-line no-catch-all/no-catch-all } catch (error) { - outputDebug(outputContent`Failed to clean up temporary file: ${outputToken.path(tempTarPath)}`) + outputDebug(`Failed to clean up temporary file: ${tempTarPath}`) } } } diff --git a/packages/cli-kit/src/public/node/base-command.ts b/packages/cli-kit/src/public/node/base-command.ts index e996d52bb53..a2272689146 100644 --- a/packages/cli-kit/src/public/node/base-command.ts +++ b/packages/cli-kit/src/public/node/base-command.ts @@ -4,7 +4,7 @@ import {isDevelopment} from './context/local.js' import {addPublicMetadata} from './metadata.js' import {AbortError} from './error.js' import {renderInfo, renderWarning} from './ui.js' -import {outputContent, outputResult, outputToken} from './output.js' +import {outputResult} from './output.js' import {terminalSupportsPrompting} from './system.js' import {hashString} from './crypto.js' import {isTruthy} from './context/utilities.js' @@ -114,11 +114,13 @@ abstract class BaseCommand extends Command { requiredFlags.forEach((name: string) => { if (!(name in flags)) { throw new AbortError( - outputContent`Flag not specified: - -${outputToken.cyan(name)} - -This flag is required in non-interactive terminal environments, such as a CI environment, or when piping input from another process.`, + [ + 'Flag not specified:', + '\n\n', + {color: {text: name, color: 'cyan'}}, + '\n\n', + 'This flag is required in non-interactive terminal environments, such as a CI environment, or when piping input from another process.', + ], 'To resolve this, specify the option in the command, or run the command in an interactive environment such as your local terminal.', ) } @@ -275,11 +277,11 @@ function argsFromEnvironment args.push(`--${label}`, `${element}`)) diff --git a/packages/cli-kit/src/public/node/context/spin.ts b/packages/cli-kit/src/public/node/context/spin.ts index 517e9cfd35f..a353bfcef12 100644 --- a/packages/cli-kit/src/public/node/context/spin.ts +++ b/packages/cli-kit/src/public/node/context/spin.ts @@ -2,18 +2,19 @@ import {isTruthy} from './utilities.js' import {fileExists, readFile, readFileSync} from '../fs.js' import {environmentVariables} from '../../../private/node/constants.js' import {captureOutput} from '../system.js' -import {outputContent, outputToken} from '../output.js' +import {stringifyMessage} from '../output.js' import {getCachedSpinFqdn, setCachedSpinFqdn} from '../../../private/node/context/spin-cache.js' import {AbortError} from '../error.js' import {Environment, serviceEnvironment} from '../../../private/node/context/service.js' import {joinPath} from '../path.js' const SpinInstanceNotFoundMessages = (spinInstance: string | undefined, error: string) => { - const errorMessage = outputContent`${outputToken.genericShellCommand( - `spin`, - )} yielded the following error trying to obtain the fully qualified domain name of the Spin instance: -${error} - ` + const errorMessage = stringifyMessage([ + {color: {text: 'spin', color: 'cyan'}}, + ' yielded the following error trying to obtain the fully qualified domain name of the Spin instance:\n', + error, + '\n ', + ]) let nextSteps: string | undefined if (spinInstance) { nextSteps = `Make sure ${spinInstance} is the instance name and not a fully qualified domain name` diff --git a/packages/cli-kit/src/public/node/dot-env.ts b/packages/cli-kit/src/public/node/dot-env.ts index 9f22b17904b..3b3b7d2e988 100644 --- a/packages/cli-kit/src/public/node/dot-env.ts +++ b/packages/cli-kit/src/public/node/dot-env.ts @@ -1,7 +1,7 @@ /* eslint-disable @typescript-eslint/no-non-null-assertion */ import {AbortError} from './error.js' import {fileExists, readFile, writeFile} from './fs.js' -import {outputDebug, outputContent, outputToken} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import {parse} from 'dotenv' /** @@ -24,7 +24,7 @@ export interface DotEnvFile { * @returns An in-memory representation of the .env file. */ export async function readAndParseDotEnv(path: string): Promise { - outputDebug(outputContent`Reading the .env file at ${outputToken.path(path)}`) + outputDebug(`Reading the .env file at ${path}`) if (!(await fileExists(path))) { throw new AbortError(`The environment file at ${path} does not exist.`) } diff --git a/packages/cli-kit/src/public/node/fs.ts b/packages/cli-kit/src/public/node/fs.ts index 0f0f361cc02..cabcd5a6412 100644 --- a/packages/cli-kit/src/public/node/fs.ts +++ b/packages/cli-kit/src/public/node/fs.ts @@ -1,5 +1,5 @@ import {joinPath, normalizePath} from './path.js' -import {outputContent, outputToken, outputDebug} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import {getRandomName, RandomNameFamily} from '../common/string.js' import {OverloadParameters} from '../../private/common/ts/overloaded-parameters.js' import { @@ -106,7 +106,7 @@ export async function readFile(path: string, options?: ReadOptions): Promise { - outputDebug(outputContent`Reading the content of file at ${outputToken.path(path)}...`) + outputDebug(`Reading the content of file at ${path}...`) // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore return fsReadFile(path, options) @@ -119,7 +119,7 @@ export async function readFile(path: string, options: ReadOptions = {encoding: ' * @returns The content of the file. */ export function readFileSync(path: string): Buffer { - outputDebug(outputContent`Sync-reading the content of file at ${outputToken.path(path)}...`) + outputDebug(`Sync-reading the content of file at ${path}...`) return fsReadFileSync(path) } @@ -140,7 +140,7 @@ export async function fileRealPath(path: string): Promise { * @param to - Destination path. */ export async function copyFile(from: string, to: string): Promise { - outputDebug(outputContent`Copying file from ${outputToken.path(from)} to ${outputToken.path(to)}...`) + outputDebug(`Copying file from ${from} to ${to}...`) await fsCopy(from, to) } @@ -150,7 +150,7 @@ export async function copyFile(from: string, to: string): Promise { * @param path - Path to the file to be created. */ export async function touchFile(path: string): Promise { - outputDebug(outputContent`Creating an empty file at ${outputToken.path(path)}...`) + outputDebug(`Creating an empty file at ${path}...`) await fsEnsureFile(path) } @@ -160,7 +160,7 @@ export async function touchFile(path: string): Promise { * @param path - Path to the file to be created. */ export function touchFileSync(path: string): void { - outputDebug(outputContent`Creating an empty file at ${outputToken.path(path)}...`) + outputDebug(`Creating an empty file at ${path}...`) fsEnsureFileSync(path) } @@ -171,7 +171,7 @@ export function touchFileSync(path: string): void { * @param content - Content to be appended. */ export async function appendFile(path: string, content: string): Promise { - outputDebug(outputContent`Appending the following content to ${outputToken.path(path)}: + outputDebug(`Appending the following content to ${path}: ${content .split('\n') .map((line) => ` ${line}`) @@ -206,7 +206,7 @@ export async function writeFile( data: string | Buffer, options: WriteOptions = {encoding: 'utf8'}, ): Promise { - outputDebug(outputContent`Writing some content to file at ${outputToken.path(path)}...`) + outputDebug(`Writing some content to file at ${path}...`) await fsWriteFile(path, data, options) } @@ -217,7 +217,7 @@ export async function writeFile( * @param data - Content to be written. */ export function writeFileSync(path: string, data: string): void { - outputDebug(outputContent`File-writing some content to file at ${outputToken.path(path)}...`) + outputDebug(`File-writing some content to file at ${path}...`) fsWriteFileSync(path, data) } @@ -227,7 +227,7 @@ export function writeFileSync(path: string, data: string): void { * @param path - Path to the directory to be created. */ export async function mkdir(path: string): Promise { - outputDebug(outputContent`Creating directory at ${outputToken.path(path)}...`) + outputDebug(`Creating directory at ${path}...`) await fsMkdir(path, {recursive: true}) } @@ -237,7 +237,7 @@ export async function mkdir(path: string): Promise { * @param path - Path to the directory to be created. */ export function mkdirSync(path: string): void { - outputDebug(outputContent`Sync-creating directory at ${outputToken.path(path)}...`) + outputDebug(`Sync-creating directory at ${path}...`) fsMkdirSync(path, {recursive: true}) } @@ -247,7 +247,7 @@ export function mkdirSync(path: string): void { * @param path - Path to the file to be removed. */ export async function removeFile(path: string): Promise { - outputDebug(outputContent`Removing file at ${outputToken.path(path)}...`) + outputDebug(`Removing file at ${path}...`) await fsRemove(path) } @@ -257,7 +257,7 @@ export async function removeFile(path: string): Promise { * @param to - New path for the file. */ export async function renameFile(from: string, to: string): Promise { - outputDebug(outputContent`Renaming file from ${outputToken.path(from)} to ${outputToken.path(to)}...`) + outputDebug(`Renaming file from ${from} to ${to}...`) await fsRename(from, to) } @@ -267,7 +267,7 @@ export async function renameFile(from: string, to: string): Promise { * @param path - Path to the file to be removed. */ export function removeFileSync(path: string): void { - outputDebug(outputContent`Sync-removing file at ${outputToken.path(path)}...`) + outputDebug(`Sync-removing file at ${path}...`) fsRemoveSync(path) } @@ -282,7 +282,7 @@ interface RmDirOptions { */ export async function rmdir(path: string, options: RmDirOptions = {}): Promise { const {default: del} = await import('del') - outputDebug(outputContent`Removing directory at ${outputToken.path(path)}...`) + outputDebug(`Removing directory at ${path}...`) await del(path, {force: options.force}) } @@ -292,7 +292,7 @@ export async function rmdir(path: string, options: RmDirOptions = {}): Promise { - outputDebug(outputContent`Creating a temporary directory...`) + outputDebug('Creating a temporary directory...') const directory = await fsMkdtemp(joinPath(os.tmpdir(), 'tmp-')) return directory } @@ -304,7 +304,7 @@ export async function mkTmpDir(): Promise { * @returns True if the path is a directory, false otherwise. */ export async function isDirectory(path: string): Promise { - outputDebug(outputContent`Checking if ${outputToken.path(path)} is a directory...`) + outputDebug(`Checking if ${path} is a directory...`) return (await fsLstat(path)).isDirectory() } @@ -315,7 +315,7 @@ export async function isDirectory(path: string): Promise { * @returns The size of the file in bytes. */ export async function fileSize(path: string): Promise { - outputDebug(outputContent`Getting the size of file file at ${outputToken.path(path)}...`) + outputDebug(`Getting the size of file file at ${path}...`) return (await fsStat(path)).size } @@ -326,7 +326,7 @@ export async function fileSize(path: string): Promise { * @returns The size of the file in bytes. */ export function fileSizeSync(path: string): number { - outputDebug(outputContent`Sync-getting the size of file file at ${outputToken.path(path)}...`) + outputDebug(`Sync-getting the size of file file at ${path}...`) return fsStatSync(path).size } @@ -380,7 +380,7 @@ export function createFileWriteStream(path: string): WriteStream { * @returns A unix timestamp. */ export async function fileLastUpdated(path: string): Promise { - outputDebug(outputContent`Getting last updated timestamp for file at ${outputToken.path(path)}...`) + outputDebug(`Getting last updated timestamp for file at ${path}...`) return (await fsStat(path)).ctime } diff --git a/packages/cli-kit/src/public/node/git.ts b/packages/cli-kit/src/public/node/git.ts index 71d93256e78..70bb3ae8c49 100644 --- a/packages/cli-kit/src/public/node/git.ts +++ b/packages/cli-kit/src/public/node/git.ts @@ -1,10 +1,9 @@ -/* eslint-disable @typescript-eslint/no-base-to-string */ import {hasGit, isTerminalInteractive} from './context/local.js' import {appendFileSync, detectEOL, fileExistsSync, readFileSync, writeFileSync} from './fs.js' import {AbortError} from './error.js' import {cwd, joinPath} from './path.js' import {runWithTimer} from './metadata.js' -import {outputContent, outputToken, outputDebug} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import git, {TaskOptions, SimpleGitProgressEvent, DefaultLogFields, ListLogLine, SimpleGit} from 'simple-git' import ignore from 'ignore' @@ -15,7 +14,7 @@ import ignore from 'ignore' * @param initialBranch - The name of the initial branch. */ export async function initializeGitRepository(directory: string, initialBranch = 'main'): Promise { - outputDebug(outputContent`Initializing git repository at ${outputToken.path(directory)}...`) + outputDebug(`Initializing git repository at ${directory}...`) await ensureGitIsPresentOrAbort() // We use init and checkout instead of `init --initial-branch` because the latter is only supported in git 2.28+ // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -52,7 +51,7 @@ export interface GitIgnoreTemplate { * @param template - The template to use to create the .gitignore file. */ export function createGitIgnore(directory: string, template: GitIgnoreTemplate): void { - outputDebug(outputContent`Creating .gitignore at ${outputToken.path(directory)}...`) + outputDebug(`Creating .gitignore at ${directory}...`) const filePath = `${directory}/.gitignore` let fileContent = '' @@ -127,7 +126,7 @@ export interface GitCloneOptions { export async function downloadGitRepository(cloneOptions: GitCloneOptions): Promise { return runWithTimer('cmd_all_timing_network_ms')(async () => { const {repoUrl, destination, progressUpdater, shallow, latestTag} = cloneOptions - outputDebug(outputContent`Git-cloning repository ${repoUrl} into ${outputToken.path(destination)}...`) + outputDebug(`Git-cloning repository ${repoUrl} into ${destination}...`) await ensureGitIsPresentOrAbort() const [repository, branch] = repoUrl.split('#') const options: TaskOptions = {'--recurse-submodules': null} @@ -211,12 +210,11 @@ export async function getLatestGitCommit(directory?: string): Promise { // @ts-ignore const ref = await git({baseDir: directory}).raw('symbolic-ref', '-q', 'HEAD') if (!ref) { - throw new AbortError( - "Git HEAD can't be detached to run command", - outputContent`Run ${outputToken.genericShellCommand( - 'git checkout [branchName]', - )} to reattach HEAD or see git ${outputToken.link( - 'documentation', - 'https://git-scm.com/book/en/v2/Git-Internals-Git-References', - )} for more details`, - ) + throw new AbortError("Git HEAD can't be detached to run command", [ + 'Run ', + {command: 'git checkout [branchName]'}, + ' to reattach HEAD or see git ', + {link: {label: 'documentation', url: 'https://git-scm.com/book/en/v2/Git-Internals-Git-References'}}, + ' for more details', + ]) } return ref.trim() } @@ -287,13 +283,10 @@ export async function getHeadSymbolicRef(directory?: string): Promise { */ export async function ensureGitIsPresentOrAbort(): Promise { if (!(await hasGit())) { - throw new AbortError( - `Git is necessary in the environment to continue`, - outputContent`Install ${outputToken.link( - 'git', - 'https://git-scm.com/book/en/v2/Getting-Started-Installing-Git', - )}`, - ) + throw new AbortError(`Git is necessary in the environment to continue`, [ + 'Install ', + {link: {label: 'git', url: 'https://git-scm.com/book/en/v2/Getting-Started-Installing-Git'}}, + ]) } } @@ -308,7 +301,7 @@ export async function ensureInsideGitDirectory(directory?: string): Promise { if (!(await isClean(directory))) { - throw new GitDirectoryNotCleanError(`${outputToken.path(directory || cwd())} is not a clean Git directory`) + throw new GitDirectoryNotCleanError(`${directory || cwd()} is not a clean Git directory`) } } diff --git a/packages/cli-kit/src/public/node/github.ts b/packages/cli-kit/src/public/node/github.ts index 9214dafd754..c302d7eb1ec 100644 --- a/packages/cli-kit/src/public/node/github.ts +++ b/packages/cli-kit/src/public/node/github.ts @@ -5,7 +5,7 @@ import {writeFile, mkdir, inTemporaryDirectory, moveFile, chmod} from './fs.js' import {dirname, joinPath} from './path.js' import {runWithTimer} from './metadata.js' import {AbortError} from './error.js' -import {outputContent, outputDebug, outputToken} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' class GitHubClientError extends Error { // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -44,7 +44,7 @@ export async function getLatestGitHubRelease( repo: string, options: GetLatestGitHubReleaseOptions = {filter: () => true}, ): Promise { - outputDebug(outputContent`Getting the latest release of GitHub repository ${owner}/${repo}...`) + outputDebug(`Getting the latest release of GitHub repository ${owner}/${repo}...`) const url = `https://api.github.com/repos/${owner}/${repo}/releases` const fetchResult = await fetch(url) // eslint-disable-next-line @typescript-eslint/no-explicit-any @@ -138,7 +138,7 @@ export async function downloadGitHubRelease( const url = `https://github.com/${repo}/releases/download/${version}/${assetName}` return runWithTimer('cmd_all_timing_network_ms')(async () => { - outputDebug(outputContent`Downloading ${outputToken.link(assetName, url)}`) + outputDebug(`Downloading ${assetName} from ${url}`) await inTemporaryDirectory(async (tmpDir) => { const tempPath = joinPath(tmpDir, assetName) let response: Response @@ -160,6 +160,6 @@ export async function downloadGitHubRelease( await mkdir(dirname(targetPath)) await moveFile(tempPath, targetPath) }) - outputDebug(outputContent`${outputToken.successIcon()} Successfully downloaded ${outputToken.path(targetPath)}`) + outputDebug(`✅ Successfully downloaded ${targetPath}`) }) } diff --git a/packages/cli-kit/src/public/node/hidden-folder.ts b/packages/cli-kit/src/public/node/hidden-folder.ts index 8b93656b702..6e63fcc7755 100644 --- a/packages/cli-kit/src/public/node/hidden-folder.ts +++ b/packages/cli-kit/src/public/node/hidden-folder.ts @@ -1,6 +1,6 @@ import {joinPath} from './path.js' import {mkdir, writeFile, fileExists} from './fs.js' -import {outputDebug, outputContent, outputToken} from './output.js' +import {outputDebug} from './output.js' const HIDDEN_FOLDER_NAME = '.shopify' @@ -17,11 +17,11 @@ export async function getOrCreateHiddenShopifyFolder(directory: string): Promise // Check if both the folder and .gitignore exist const [folderExists, gitignoreExists] = await Promise.all([fileExists(hiddenFolder), fileExists(gitignorePath)]) if (!folderExists) { - outputDebug(outputContent`Creating hidden .shopify folder at ${outputToken.path(hiddenFolder)}...`) + outputDebug(`Creating hidden .shopify folder at ${hiddenFolder}...`) await mkdir(hiddenFolder) } if (!gitignoreExists) { - outputDebug(outputContent`Creating .gitignore in ${outputToken.path(hiddenFolder)}...`) + outputDebug(`Creating .gitignore in ${hiddenFolder}...`) await writeFile(gitignorePath, `# Ignore the entire ${HIDDEN_FOLDER_NAME} directory\n*`) } return hiddenFolder diff --git a/packages/cli-kit/src/public/node/http.ts b/packages/cli-kit/src/public/node/http.ts index cb828eb28c2..85f35ab23da 100644 --- a/packages/cli-kit/src/public/node/http.ts +++ b/packages/cli-kit/src/public/node/http.ts @@ -5,7 +5,7 @@ import {runWithTimer} from './metadata.js' import {maxRequestTimeForNetworkCallsMs, skipNetworkLevelRetry} from './environment.js' import {httpsAgent, sanitizedHeadersOutput} from '../../private/node/api/headers.js' import {sanitizeURL} from '../../private/node/api/urls.js' -import {outputContent, outputDebug, outputToken} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import {NetworkRetryBehaviour, simpleRequestWithDebugLog} from '../../private/node/api.js' import {DEFAULT_MAX_TIME_MS} from '../../private/node/sleep-with-backoff.js' import FormData from 'form-data' @@ -118,7 +118,7 @@ export function abortSignalFromRequestBehaviour(behaviour: RequestBehaviour): Ab async function innerFetch({url, behaviour, init, logRequest, useHttpsAgent}: FetchOptions): Promise { if (logRequest) { - outputDebug(outputContent`Sending ${init?.method ?? 'GET'} request to URL ${sanitizeURL(url.toString())} + outputDebug(`Sending ${init?.method ?? 'GET'} request to URL ${sanitizeURL(url.toString())} With request headers: ${sanitizedHeadersOutput((init?.headers ?? {}) as {[header: string]: string})} `) @@ -238,7 +238,7 @@ export function downloadFile(url: string, to: string): Promise { unlinkFileSync(to) // eslint-disable-next-line no-catch-all/no-catch-all } catch (err: unknown) { - outputDebug(outputContent`Failed to remove file ${outputToken.path(to)}: ${outputToken.raw(String(err))}`) + outputDebug(`Failed to remove file ${to}: ${String(err)}`) } } diff --git a/packages/cli-kit/src/public/node/liquid.ts b/packages/cli-kit/src/public/node/liquid.ts index 95640c0a442..c44d25f9196 100644 --- a/packages/cli-kit/src/public/node/liquid.ts +++ b/packages/cli-kit/src/public/node/liquid.ts @@ -11,7 +11,7 @@ import { matchGlob, } from './fs.js' import {joinPath, dirname, relativePath} from './path.js' -import {outputContent, outputToken, outputDebug} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import {Liquid} from 'liquidjs' /** @@ -37,7 +37,7 @@ export function renderLiquidTemplate(templateContent: string, data: object): Pro * @param data - Data to feed the template engine. */ export async function recursiveLiquidTemplateCopy(from: string, to: string, data: object): Promise { - outputDebug(outputContent`Copying template from directory ${outputToken.path(from)} to ${outputToken.path(to)}`) + outputDebug(`Copying template from directory ${from} to ${to}`) const templateFiles: string[] = await glob(joinPath(from, '**/*'), {dot: true}) const bypassPaths = joinPath(from, '.cli-liquid-bypass') diff --git a/packages/cli-kit/src/public/node/monorail.ts b/packages/cli-kit/src/public/node/monorail.ts index 65bfd1eda9d..681d47fc255 100644 --- a/packages/cli-kit/src/public/node/monorail.ts +++ b/packages/cli-kit/src/public/node/monorail.ts @@ -1,6 +1,6 @@ import {fetch} from './http.js' import {JsonMap} from '../../private/common/json.js' -import {outputDebug, outputContent, outputToken} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import {DeepRequired} from '../common/ts/deep-required.js' export {DeepRequired} @@ -203,7 +203,7 @@ export async function publishMonorailEvent { const getLatestVersion = async () => { - outputDebug(outputContent`Checking if there's a version of ${dependency} newer than ${currentVersion}`) + outputDebug(`Checking if there's a version of ${dependency} newer than ${currentVersion}`) return getLatestNPMPackageVersion(dependency) } @@ -472,10 +472,10 @@ export async function addNPMDependenciesIfNeeded( dependencies: DependencyVersion[], options: AddNPMDependenciesIfNeededOptions, ): Promise { - outputDebug(outputContent`Adding the following dependencies if needed: -${outputToken.json(dependencies)} + outputDebug(`Adding the following dependencies if needed: +${JSON.stringify(dependencies, null, 2)} With options: -${outputToken.json(options)} +${JSON.stringify(options, null, 2)} `) const packageJsonPath = joinPath(options.directory, 'package.json') if (!(await fileExists(packageJsonPath))) { @@ -705,7 +705,7 @@ export async function addResolutionOrOverride(directory: string, dependencies: { * @returns A promise to get the latest available version of a package. */ async function getLatestNPMPackageVersion(name: string) { - outputDebug(outputContent`Getting the latest version of NPM package: ${outputToken.raw(name)}`) + outputDebug(`Getting the latest version of NPM package: ${name}`) return runWithTimer('cmd_all_timing_network_ms')(() => { return latestVersion(name) }) @@ -718,7 +718,7 @@ async function getLatestNPMPackageVersion(name: string) { * @param packageJSON - Package.json file to write. */ export async function writePackageJSON(directory: string, packageJSON: PackageJson): Promise { - outputDebug(outputContent`JSON-encoding and writing content to package.json at ${outputToken.path(directory)}...`) + outputDebug(`JSON-encoding and writing content to package.json at ${directory}...`) const packagePath = joinPath(directory, 'package.json') await writeFile(packagePath, JSON.stringify(packageJSON, null, 2)) } diff --git a/packages/cli-kit/src/public/node/os.ts b/packages/cli-kit/src/public/node/os.ts index a05fbf6e9e7..0cd4aea1eac 100644 --- a/packages/cli-kit/src/public/node/os.ts +++ b/packages/cli-kit/src/public/node/os.ts @@ -1,4 +1,4 @@ -import {outputDebug, outputContent} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import {execa} from 'execa' import {userInfo as osUserInfo} from 'os' @@ -10,7 +10,7 @@ import {userInfo as osUserInfo} from 'os' * @returns The username of the current user. */ export async function username(platform: typeof process.platform = process.platform): Promise { - outputDebug(outputContent`Obtaining user name...`) + outputDebug('Obtaining user name...') const environmentVariable = getEnvironmentVariable() if (environmentVariable) { return environmentVariable diff --git a/packages/cli-kit/src/public/node/output.ts b/packages/cli-kit/src/public/node/output.ts index db95c2ebd14..d1e31a8bfb9 100644 --- a/packages/cli-kit/src/public/node/output.ts +++ b/packages/cli-kit/src/public/node/output.ts @@ -134,6 +134,9 @@ export function formatPackageManagerCommand( /** * Creates a tokenized string from an array of strings and tokens. * + * @deprecated This function is deprecated. Use stringifyMessage with token arrays instead. + * For example: stringifyMessage([{bold: 'text'}, '\n', body]) instead of outputContent`${outputToken.heading('text')}\n${body}`.value. + * * @param strings - The strings to join. * @param keys - Array of tokens or strings to join. * @returns The tokenized string. @@ -345,9 +348,39 @@ export function outputNewline(): void { export function stringifyMessage(message: OutputMessage): string { if (message instanceof TokenizedString) { return message.value - } else { + } else if (typeof message === 'string') { + return message + } else if (message && typeof message === 'object' && 'body' in message) { + // Handle complex token objects like those used in upgrade.ts + const body = (message as unknown).body + if (Array.isArray(body)) { + return body + .map((item) => { + if (typeof item === 'string') { + return item + } else if (item && typeof item === 'object') { + return itemToString(item) + } + return String(item) + }) + .join('') + } + // If body is not an array, fall through to handle as regular object + return String((message as unknown).body) + } else if (Array.isArray(message)) { + // Handle direct arrays like those used in version-name validation return message + .map((item) => { + if (typeof item === 'string') { + return item + } else if (item && typeof item === 'object') { + return itemToString(item) + } + return String(item) + }) + .join('') // Concatenate array elements directly } + return String(message) } /** @@ -425,5 +458,5 @@ export function shouldDisplayColors(_process = process): boolean { */ export function formatSection(title: string, body: string): string { const formattedTitle = `${title.toUpperCase()}${' '.repeat(35 - title.length)}` - return outputContent`${outputToken.heading(formattedTitle)}\n${body}`.value + return stringifyMessage([{bold: formattedTitle}, '\n', body]) } diff --git a/packages/cli-kit/src/public/node/session.ts b/packages/cli-kit/src/public/node/session.ts index 9493f05d479..a11b5d19665 100644 --- a/packages/cli-kit/src/public/node/session.ts +++ b/packages/cli-kit/src/public/node/session.ts @@ -8,7 +8,7 @@ import { exchangeCliTokenForAppManagementAccessToken, exchangeCliTokenForBusinessPlatformAccessToken, } from '../../private/node/session/exchange.js' -import {outputContent, outputToken, outputDebug} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import { AdminAPIScope, AppManagementAPIScope, @@ -49,8 +49,8 @@ export async function ensureAuthenticatedPartners( env = process.env, options: EnsureAuthenticatedAdditionalOptions = {}, ): Promise<{token: string; userId: string}> { - outputDebug(outputContent`Ensuring that the user is authenticated with the Partners API with the following scopes: -${outputToken.json(scopes)} + outputDebug(`Ensuring that the user is authenticated with the Partners API with the following scopes: +${JSON.stringify(scopes, null, 2)} `) const envToken = getPartnersToken() if (envToken) { @@ -79,8 +79,8 @@ export async function ensureAuthenticatedAppManagementAndBusinessPlatform( businessPlatformScopes: BusinessPlatformScope[] = [], env = process.env, ): Promise<{appManagementToken: string; userId: string; businessPlatformToken: string}> { - outputDebug(outputContent`Ensuring that the user is authenticated with the App Management API with the following scopes: -${outputToken.json(appManagementScopes)} + outputDebug(`Ensuring that the user is authenticated with the App Management API with the following scopes: +${JSON.stringify(appManagementScopes, null, 2)} `) const envToken = getPartnersToken() @@ -132,8 +132,8 @@ export async function ensureAuthenticatedStorefront( return password } - outputDebug(outputContent`Ensuring that the user is authenticated with the Storefront API with the following scopes: -${outputToken.json(scopes)} + outputDebug(`Ensuring that the user is authenticated with the Storefront API with the following scopes: +${JSON.stringify(scopes, null, 2)} `) const tokens = await ensureAuthenticated({storefrontRendererApi: {scopes}}, process.env, {forceRefresh}) if (!tokens.storefront) { @@ -157,10 +157,8 @@ export async function ensureAuthenticatedAdmin( forceRefresh = false, options: EnsureAuthenticatedAdditionalOptions = {}, ): Promise { - outputDebug(outputContent`Ensuring that the user is authenticated with the Admin API with the following scopes for the store ${outputToken.raw( - store, - )}: -${outputToken.json(scopes)} + outputDebug(`Ensuring that the user is authenticated with the Admin API with the following scopes for the store ${store}: +${JSON.stringify(scopes, null, 2)} `) const tokens = await ensureAuthenticated({adminApi: {scopes, storeFqdn: store}}, process.env, { forceRefresh, @@ -189,8 +187,8 @@ export async function ensureAuthenticatedThemes( scopes: AdminAPIScope[] = [], forceRefresh = false, ): Promise { - outputDebug(outputContent`Ensuring that the user is authenticated with the Theme API with the following scopes: -${outputToken.json(scopes)} + outputDebug(`Ensuring that the user is authenticated with the Theme API with the following scopes: +${JSON.stringify(scopes, null, 2)} `) if (password) { const session = {token: password, storeFqdn: await normalizeStoreFqdn(store)} @@ -209,8 +207,8 @@ ${outputToken.json(scopes)} * @returns The access token for the Business Platform API. */ export async function ensureAuthenticatedBusinessPlatform(scopes: BusinessPlatformScope[] = []): Promise { - outputDebug(outputContent`Ensuring that the user is authenticated with the Business Platform API with the following scopes: -${outputToken.json(scopes)} + outputDebug(`Ensuring that the user is authenticated with the Business Platform API with the following scopes: +${JSON.stringify(scopes, null, 2)} `) const tokens = await ensureAuthenticated({businessPlatformApi: {scopes}}, process.env) if (!tokens.businessPlatform) { diff --git a/packages/cli-kit/src/public/node/tcp.ts b/packages/cli-kit/src/public/node/tcp.ts index 76dda25feeb..3fc645c1aad 100644 --- a/packages/cli-kit/src/public/node/tcp.ts +++ b/packages/cli-kit/src/public/node/tcp.ts @@ -1,6 +1,6 @@ import {sleep} from './system.js' import {AbortError} from './error.js' -import {outputDebug, outputContent, outputToken} from '../../public/node/output.js' +import {outputDebug} from '../../public/node/output.js' import * as port from 'get-port-please' interface GetTCPPortOptions { @@ -19,10 +19,10 @@ const obtainedRandomPorts = new Set() */ export async function getAvailableTCPPort(preferredPort?: number, options?: GetTCPPortOptions): Promise { if (preferredPort && (await checkPortAvailability(preferredPort))) { - outputDebug(outputContent`Port ${preferredPort.toString()} is free`) + outputDebug(`Port ${preferredPort.toString()} is free`) return preferredPort } - outputDebug(outputContent`Getting a random port...`) + outputDebug('Getting a random port...') let randomPort = await retryOnError(() => port.getRandomPort(host()), options?.maxTries, options?.waitTimeInSeconds) for (let i = 0; i < (options?.maxTries ?? 5); i++) { @@ -33,7 +33,7 @@ export async function getAvailableTCPPort(preferredPort?: number, options?: GetT randomPort = await retryOnError(() => port.getRandomPort(host()), options?.maxTries, options?.waitTimeInSeconds) } - outputDebug(outputContent`Random port obtained: ${outputToken.raw(`${randomPort}`)}`) + outputDebug(`Random port obtained: ${randomPort}`) obtainedRandomPorts.add(randomPort) return randomPort } @@ -70,7 +70,7 @@ async function retryOnError(execute: () => T, maxTries = 5, waitTimeInSeconds // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { if (retryCount++ < maxTries) { - outputDebug(outputContent`Unknown problem getting a random port: ${error.message}`) + outputDebug(`Unknown problem getting a random port: ${error.message}`) // eslint-disable-next-line no-await-in-loop await sleep(waitTimeInSeconds) } else { diff --git a/packages/cli-kit/src/public/node/themes/conf.ts b/packages/cli-kit/src/public/node/themes/conf.ts index c05c603ef05..9c6da43e20a 100644 --- a/packages/cli-kit/src/public/node/themes/conf.ts +++ b/packages/cli-kit/src/public/node/themes/conf.ts @@ -1,6 +1,6 @@ import {LocalStorage} from '@shopify/cli-kit/node/local-storage' import {AdminSession} from '@shopify/cli-kit/node/session' -import {outputDebug, outputContent} from '@shopify/cli-kit/node/output' +import {outputDebug} from '@shopify/cli-kit/node/output' type HostThemeId = string type StoreFqdn = AdminSession['storeFqdn'] @@ -21,16 +21,16 @@ export function hostThemeLocalStorage(): LocalStorage { - outputDebug( - outputContent`Checking if the directory ${outputToken.path( - root, - )} or any of its parents has a .vscode directory... `, - ) + outputDebug(`Checking if the directory ${root} or any of its parents has a .vscode directory...`) const config = await findPathUp(joinPath(root, '.vscode'), {type: 'directory'}) if (!config) { @@ -30,9 +26,7 @@ export async function isVSCode(root = cwd()): Promise { * @param recommendations - List of VSCode extensions to recommend. */ export async function addRecommendedExtensions(directory: string, recommendations: string[]): Promise { - outputDebug(outputContent`Adding VSCode recommended extensions at ${outputToken.path(directory)}: -${outputToken.json(recommendations)} - `) + outputDebug(`Adding VSCode recommended extensions at ${directory}: ${JSON.stringify(recommendations, null, 2)}`) const extensionsPath = joinPath(directory, '.vscode/extensions.json') if (await isVSCode(directory)) { diff --git a/packages/cli/src/cli/services/upgrade.test.ts b/packages/cli/src/cli/services/upgrade.test.ts index 08407731792..ecd8be95952 100644 --- a/packages/cli/src/cli/services/upgrade.test.ts +++ b/packages/cli/src/cli/services/upgrade.test.ts @@ -60,7 +60,8 @@ describe('upgrade global CLI', () => { {stdio: 'inherit'}, ) expect(outputMock.info()).toMatchInlineSnapshot(` - "Upgrading CLI from ${oldCliVersion} to ${currentCliVersion}...\nAttempting to upgrade via \`npm install -g @shopify/cli@latest @shopify/theme@latest\`..." + "Upgrading CLI from 3.0.0 to 3.10.0... + Attempting to upgrade via npm install -g @shopify/cli@latest @shopify/theme@latest..." `) expect(outputMock.success()).toMatchInlineSnapshot(` "Upgraded Shopify CLI to version ${currentCliVersion}" diff --git a/packages/cli/src/cli/services/upgrade.ts b/packages/cli/src/cli/services/upgrade.ts index 0ae3524bd3d..c93ef6a2d43 100644 --- a/packages/cli/src/cli/services/upgrade.ts +++ b/packages/cli/src/cli/services/upgrade.ts @@ -12,7 +12,7 @@ import {exec} from '@shopify/cli-kit/node/system' import {dirname, joinPath, moduleDirectory} from '@shopify/cli-kit/node/path' import {findPathUp, glob} from '@shopify/cli-kit/node/fs' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputInfo, outputSuccess, outputToken, outputWarn} from '@shopify/cli-kit/node/output' +import {outputInfo, outputSuccess, outputWarn} from '@shopify/cli-kit/node/output' type HomebrewPackageName = 'shopify-cli' | 'shopify-cli@3' @@ -34,11 +34,11 @@ export async function upgrade( if (projectDir) { newestVersion = await upgradeLocalShopify(projectDir, currentVersion) } else if (usingPackageManager({env})) { - throw new AbortError( - outputContent`Couldn't find an app toml file at ${outputToken.path( - directory, - )}, is this a Shopify project directory?`, - ) + throw new AbortError("Couldn't find an app toml file, is this a Shopify project directory?", [ + "Couldn't find an app toml file at ", + {filePath: directory}, + ', is this a Shopify project directory?', + ]) } else { newestVersion = await upgradeGlobalShopify(currentVersion, {env}) } @@ -105,11 +105,11 @@ async function upgradeGlobalShopify( const homebrewPackage = env.SHOPIFY_HOMEBREW_FORMULA as HomebrewPackageName | undefined try { if (homebrewPackage) { - throw new AbortError( - outputContent`Upgrade only works for packages managed by a Node package manager (e.g. npm). Run ${outputToken.genericShellCommand( - 'brew upgrade && brew update', - )} instead`, - ) + throw new AbortError('Upgrade only works for packages managed by a Node package manager (e.g. npm)', [ + 'Upgrade only works for packages managed by a Node package manager (e.g. npm). Run ', + {command: 'brew upgrade && brew update'}, + ' instead', + ]) } else { await upgradeGlobalViaNpm() } @@ -128,20 +128,28 @@ async function upgradeGlobalViaNpm(): Promise { `${await cliDependency()}@latest`, ...globalPlugins.map((plugin) => `${plugin}@latest`), ] - outputInfo( - outputContent`Attempting to upgrade via ${outputToken.genericShellCommand([command, ...args].join(' '))}...`, - ) + outputInfo({ + body: ['Attempting to upgrade via ', {command: [command, ...args].join(' ')}, '...'], + }) await exec(command, args, {stdio: 'inherit'}) } function outputWontInstallMessage(currentVersion: string): void { - outputInfo(outputContent`You're on the latest version, ${outputToken.yellow(currentVersion)}, no need to upgrade!`) + outputInfo({ + body: ["You're on the latest version, ", {color: {text: currentVersion, color: 'yellow'}}, ', no need to upgrade!'], + }) } function outputUpgradeMessage(currentVersion: string, newestVersion: string): void { - outputInfo( - outputContent`Upgrading CLI from ${outputToken.yellow(currentVersion)} to ${outputToken.yellow(newestVersion)}...`, - ) + outputInfo({ + body: [ + 'Upgrading CLI from ', + {color: {text: currentVersion, color: 'yellow'}}, + ' to ', + {color: {text: newestVersion, color: 'yellow'}}, + '...', + ], + }) } async function installJsonDependencies( diff --git a/packages/theme/src/cli/services/local-storage.ts b/packages/theme/src/cli/services/local-storage.ts index d07f5909862..22eada48953 100644 --- a/packages/theme/src/cli/services/local-storage.ts +++ b/packages/theme/src/cli/services/local-storage.ts @@ -1,6 +1,6 @@ import {BugError} from '@shopify/cli-kit/node/error' import {LocalStorage} from '@shopify/cli-kit/node/local-storage' -import {outputDebug, outputContent} from '@shopify/cli-kit/node/output' +import {outputDebug} from '@shopify/cli-kit/node/output' type DevelopmentThemeId = string @@ -66,7 +66,7 @@ export function setThemeStore(store: string, storage: LocalStorage = themeLocalStorage(), ): string | undefined { - outputDebug(outputContent`Getting development theme...`) + outputDebug('Getting development theme...') return developmentThemeLocalStorage().get(assertThemeStoreExists(themeStorage)) } @@ -74,21 +74,21 @@ export function setDevelopmentTheme( theme: string, themeStorage: LocalStorage = themeLocalStorage(), ): void { - outputDebug(outputContent`Setting development theme...`) + outputDebug('Setting development theme...') developmentThemeLocalStorage().set(assertThemeStoreExists(themeStorage), theme) } export function removeDevelopmentTheme( themeStorage: LocalStorage = themeLocalStorage(), ): void { - outputDebug(outputContent`Removing development theme...`) + outputDebug('Removing development theme...') developmentThemeLocalStorage().delete(assertThemeStoreExists(themeStorage)) } export function getREPLTheme( themeStorage: LocalStorage = themeLocalStorage(), ): string | undefined { - outputDebug(outputContent`Getting REPL theme...`) + outputDebug('Getting REPL theme...') return replThemeLocalStorage().get(assertThemeStoreExists(themeStorage)) } @@ -96,12 +96,12 @@ export function setREPLTheme( theme: string, themeStorage: LocalStorage = themeLocalStorage(), ): void { - outputDebug(outputContent`Setting REPL theme to ${theme}...`) + outputDebug(`Setting REPL theme to ${theme}...`) replThemeLocalStorage().set(assertThemeStoreExists(themeStorage), theme) } export function removeREPLTheme(themeStorage: LocalStorage = themeLocalStorage()): void { - outputDebug(outputContent`Removing REPL theme...`) + outputDebug('Removing REPL theme...') replThemeLocalStorage().delete(assertThemeStoreExists(themeStorage)) } @@ -109,7 +109,7 @@ export function getStorefrontPassword( themeStorage: LocalStorage = themeLocalStorage(), ): string | undefined { const themeStore = assertThemeStoreExists(themeStorage) - outputDebug(outputContent`Getting storefront password for shop ${themeStore}...`) + outputDebug(`Getting storefront password for shop ${themeStore}...`) return themeStorePasswordStorage().get(themeStore) } @@ -118,7 +118,7 @@ export function setStorefrontPassword( themeStorage: LocalStorage = themeLocalStorage(), ): void { const themeStore = assertThemeStoreExists(themeStorage) - outputDebug(outputContent`Setting storefront password for shop ${themeStore}...`) + outputDebug(`Setting storefront password for shop ${themeStore}...`) themeStorePasswordStorage().set(themeStore, password) } @@ -126,7 +126,7 @@ export function removeStorefrontPassword( themeStorage: LocalStorage = themeLocalStorage(), ): void { const themeStore = assertThemeStoreExists(themeStorage) - outputDebug(outputContent`Removing storefront password for ${themeStore}...`) + outputDebug(`Removing storefront password for ${themeStore}...`) themeStorePasswordStorage().delete(themeStore) } diff --git a/packages/theme/src/cli/utilities/log-request-line.ts b/packages/theme/src/cli/utilities/log-request-line.ts index 1d8db20b6f8..52f424afe5f 100644 --- a/packages/theme/src/cli/utilities/log-request-line.ts +++ b/packages/theme/src/cli/utilities/log-request-line.ts @@ -1,7 +1,6 @@ -/* eslint-disable @typescript-eslint/unbound-method */ import {EXTENSION_CDN_PREFIX, VANITY_CDN_PREFIX} from './theme-environment/proxy.js' import {timestampDateFormat} from '../constants.js' -import {outputContent, outputInfo, outputToken} from '@shopify/cli-kit/node/output' +import {outputInfo} from '@shopify/cli-kit/node/output' import {H3Event} from 'h3' import {extname} from '@shopify/cli-kit/node/path' @@ -23,17 +22,24 @@ export function logRequestLine(event: H3Event, response: MinimalResponse) { const requestDuration = serverTiming?.match(/cfRequestDuration;dur=([\d.]+)/)?.[1] const durationString = requestDuration ? `${Math.round(Number(requestDuration))}ms` : '' - const statusColor = getColorizeStatus(response.status) + const statusColorToken = getColorizeStatus(response.status) const eventMethodAligned = event.method.padStart(6) - outputInfo( - outputContent`• ${timestampDateFormat.format(new Date())} Request ${outputToken.raw( - '»', - )} ${eventMethodAligned} ${statusColor(String(response.status))} ${truncatedPath} ${outputToken.gray( - durationString, - )}`, - ) + outputInfo({ + body: [ + '• ', + timestampDateFormat.format(new Date()), + ' Request » ', + eventMethodAligned, + ' ', + statusColorToken, + ' ', + truncatedPath, + ' ', + {color: {text: durationString, color: 'gray'}}, + ], + }) } export function shouldLog(event: H3Event) { @@ -52,10 +58,10 @@ export function shouldLog(event: H3Event) { function getColorizeStatus(status: number) { if (status < 300) { - return outputToken.green + return {color: {text: String(status), color: 'green'}} } else if (status < 400) { - return outputToken.yellow + return {color: {text: String(status), color: 'yellow'}} } else { - return outputToken.errorText + return {color: {text: String(status), color: 'red'}} } } diff --git a/packages/theme/src/cli/utilities/repl/evaluator.test.ts b/packages/theme/src/cli/utilities/repl/evaluator.test.ts index d0e745348fc..b0c3c10cabc 100644 --- a/packages/theme/src/cli/utilities/repl/evaluator.test.ts +++ b/packages/theme/src/cli/utilities/repl/evaluator.test.ts @@ -2,7 +2,7 @@ import {evaluate, EvaluationConfig} from './evaluator.js' import {DevServerSession} from '../theme-environment/types.js' import {render} from '../theme-environment/storefront-renderer.js' import {beforeEach, describe, expect, test, vi} from 'vitest' -import {outputContent, outputInfo, outputToken} from '@shopify/cli-kit/node/output' +import {outputInfo, stringifyMessage} from '@shopify/cli-kit/node/output' import {AbortError} from '@shopify/cli-kit/node/error' vi.mock('../theme-environment/storefront-renderer') @@ -112,7 +112,7 @@ Liquid syntax error (snippets/eval line 1): Unknown tag 'invalid_tag'`, expect(result).toBeUndefined() expect(outputInfo).toHaveBeenCalledOnce() expect(outputInfo).toHaveBeenCalledWith( - outputContent`${outputToken.errorText("Unknown object, property, tag, or filter: 'invalid_tag'")}`, + stringifyMessage([{error: "Unknown object, property, tag, or filter: 'invalid_tag'"}]), ) }) @@ -130,7 +130,7 @@ Liquid syntax error (snippets/eval line 1): Liquid error: undefined method 'unkn expect(result).toBeUndefined() expect(outputInfo).toHaveBeenCalledOnce() expect(outputInfo).toHaveBeenCalledWith( - outputContent`${outputToken.errorText("Liquid error: undefined method 'unknown_object' for nil:NilClass")}`, + stringifyMessage([{error: "Liquid error: undefined method 'unknown_object' for nil:NilClass"}]), ) }) diff --git a/packages/theme/src/cli/utilities/repl/evaluator.ts b/packages/theme/src/cli/utilities/repl/evaluator.ts index a69edc26be2..79dadc1c01f 100644 --- a/packages/theme/src/cli/utilities/repl/evaluator.ts +++ b/packages/theme/src/cli/utilities/repl/evaluator.ts @@ -1,7 +1,7 @@ import {render} from '../theme-environment/storefront-renderer.js' import {DevServerSession} from '../theme-environment/types.js' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputDebug, outputInfo, outputToken} from '@shopify/cli-kit/node/output' +import {outputDebug, outputInfo, stringifyMessage} from '@shopify/cli-kit/node/output' export interface SessionItem { type: string @@ -22,7 +22,7 @@ export async function evaluate(config: EvaluationConfig): Promise ${config.snippet}`)}`) + outputInfo(stringifyMessage([{color: {text: `> ${config.snippet}`, color: 'gray'}}])) return evalContext(config) } } @@ -91,13 +91,13 @@ async function evalSyntaxError(config: EvaluationConfig) { function printSyntaxError(snippet: string, error: string) { if (error.includes('Unknown tag')) { - outputInfo(outputContent`${outputToken.errorText(`Unknown object, property, tag, or filter: '${snippet}'`)}`) + outputInfo(stringifyMessage([{error: `Unknown object, property, tag, or filter: '${snippet}'`}])) return } const resultContent = stripHTMLContent(error) if (resultContent) { - outputInfo(outputContent`${outputToken.errorText(resultContent)}`) + outputInfo(stringifyMessage([{error: resultContent}])) } } diff --git a/packages/theme/src/cli/utilities/repl/presenter.test.ts b/packages/theme/src/cli/utilities/repl/presenter.test.ts index f9234a061dd..c604bbde4e7 100644 --- a/packages/theme/src/cli/utilities/repl/presenter.test.ts +++ b/packages/theme/src/cli/utilities/repl/presenter.test.ts @@ -1,5 +1,5 @@ import {presentValue} from './presenter.js' -import {outputContent, outputInfo, outputToken} from '@shopify/cli-kit/node/output' +import {outputInfo} from '@shopify/cli-kit/node/output' import {describe, expect, test, vi} from 'vitest' vi.mock('@shopify/cli-kit/node/output') @@ -28,7 +28,9 @@ describe('presentValue', () => { presentValue(value) // Then - expect(outputInfo).toHaveBeenCalledWith(outputContent`${outputToken.cyan('null')}`) + expect(outputInfo).toHaveBeenCalledWith({ + body: [{color: {text: 'null', color: 'cyan'}}], + }) expect(outputInfo).not.toHaveBeenCalledWith(cantBePrintedMessage) }) @@ -40,7 +42,9 @@ describe('presentValue', () => { presentValue(value) // Then - expect(outputInfo).toHaveBeenCalledWith(outputContent`${outputToken.cyan('null')}`) + expect(outputInfo).toHaveBeenCalledWith({ + body: [{color: {text: 'null', color: 'cyan'}}], + }) expect(outputInfo).not.toHaveBeenCalledWith(cantBePrintedMessage) }) @@ -53,7 +57,9 @@ describe('presentValue', () => { presentValue(value) // Then - expect(outputInfo).toHaveBeenCalledWith(outputContent`${outputToken.cyan(formattedOutput)}`) + expect(outputInfo).toHaveBeenCalledWith({ + body: [{color: {text: formattedOutput, color: 'cyan'}}], + }) expect(outputInfo).not.toHaveBeenCalledWith(cantBePrintedMessage) }) }) diff --git a/packages/theme/src/cli/utilities/repl/presenter.ts b/packages/theme/src/cli/utilities/repl/presenter.ts index 72bae9827e4..518d985a671 100644 --- a/packages/theme/src/cli/utilities/repl/presenter.ts +++ b/packages/theme/src/cli/utilities/repl/presenter.ts @@ -1,4 +1,4 @@ -import {outputContent, outputInfo, outputToken} from '@shopify/cli-kit/node/output' +import {outputInfo} from '@shopify/cli-kit/node/output' export function presentValue(value?: unknown) { if (hasJsonError(value)) { @@ -33,5 +33,7 @@ function hasJsonError(output: unknown): boolean { } function renderValue(value: string) { - return outputInfo(outputContent`${outputToken.cyan(value)}`) + return outputInfo({ + body: [{color: {text: value, color: 'cyan'}}], + }) } diff --git a/packages/theme/src/cli/utilities/theme-environment/dev-server-session.test.ts b/packages/theme/src/cli/utilities/theme-environment/dev-server-session.test.ts index 39bc6cf82da..3757c17306e 100644 --- a/packages/theme/src/cli/utilities/theme-environment/dev-server-session.test.ts +++ b/packages/theme/src/cli/utilities/theme-environment/dev-server-session.test.ts @@ -8,7 +8,7 @@ import {ensureAuthenticatedStorefront, ensureAuthenticatedThemes} from '@shopify import {fetchThemeAssets, themeDelete} from '@shopify/cli-kit/node/themes/api' import {describe, expect, test, vi, beforeEach} from 'vitest' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {stringifyMessage} from '@shopify/cli-kit/node/output' vi.mock('@shopify/cli-kit/node/session') vi.mock('@shopify/cli-kit/node/themes/api') @@ -49,9 +49,13 @@ describe('getStorefrontSessionCookiesWithVerification', () => { // Then await expect(cookiesWithVerification).rejects.toThrow( new AbortError( - outputContent`Theme ${outputToken.cyan(themeId)} is missing required files. Run ${outputToken.cyan( - `shopify theme delete -t ${themeId}`, - )} to delete it, then try your command again.`.value, + stringifyMessage([ + 'Theme ', + {color: {text: themeId, color: 'cyan'}}, + ' is missing required files. Run ', + {color: {text: `shopify theme delete -t ${themeId}`, color: 'cyan'}}, + ' to delete it, then try your command again.', + ]), ), ) }) diff --git a/packages/theme/src/cli/utilities/theme-environment/dev-server-session.ts b/packages/theme/src/cli/utilities/theme-environment/dev-server-session.ts index e1a4caab144..9c8b7a37011 100644 --- a/packages/theme/src/cli/utilities/theme-environment/dev-server-session.ts +++ b/packages/theme/src/cli/utilities/theme-environment/dev-server-session.ts @@ -3,7 +3,7 @@ import {getStorefrontSessionCookies, ShopifyEssentialError} from './storefront-s import {DevServerSession} from './types.js' import {fetchThemeAssets} from '@shopify/cli-kit/node/themes/api' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputDebug, outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {outputDebug, stringifyMessage} from '@shopify/cli-kit/node/output' import {AdminSession, ensureAuthenticatedStorefront, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' // 30 minutes in miliseconds. @@ -110,9 +110,13 @@ export async function abortOnMissingRequiredFile(themeId: string, adminSession: if (requiredAssets.length !== REQUIRED_THEME_FILES.length) { throw new AbortError( - outputContent`Theme ${outputToken.cyan(themeId)} is missing required files. Run ${outputToken.cyan( - `shopify theme delete -t ${themeId}`, - )} to delete it, then try your command again.`.value, + stringifyMessage([ + 'Theme ', + {color: {text: themeId, color: 'cyan'}}, + ' is missing required files. Run ', + {color: {text: `shopify theme delete -t ${themeId}`, color: 'cyan'}}, + ' to delete it, then try your command again.', + ]), ) } diff --git a/packages/theme/src/cli/utilities/theme-environment/theme-polling.ts b/packages/theme/src/cli/utilities/theme-environment/theme-polling.ts index 4ecf2f79f45..df2b5fbaace 100644 --- a/packages/theme/src/cli/utilities/theme-environment/theme-polling.ts +++ b/packages/theme/src/cli/utilities/theme-environment/theme-polling.ts @@ -3,7 +3,7 @@ import {batchedRequests} from '../batching.js' import {renderThrownError} from '../errors.js' import {Checksum, Theme, ThemeFileSystem} from '@shopify/cli-kit/node/themes/types' import {fetchChecksums, fetchThemeAssets} from '@shopify/cli-kit/node/themes/api' -import {outputDebug, outputInfo, outputContent, outputToken} from '@shopify/cli-kit/node/output' +import {outputDebug, outputInfo} from '@shopify/cli-kit/node/output' import {AdminSession} from '@shopify/cli-kit/node/session' import {renderFatalError} from '@shopify/cli-kit/node/ui' import {AbortError} from '@shopify/cli-kit/node/error' @@ -136,11 +136,7 @@ async function syncChangedAssets( assets.map(async (asset) => { if (asset) { await localFileSystem.write(asset) - outputInfo( - outputContent`• ${timestampDateFormat.format(new Date())} Synced ${outputToken.raw( - '»', - )} ${outputToken.gray(`download ${asset.key} from remote theme`)}`, - ) + outputInfo(`• ${timestampDateFormat.format(new Date())} Synced » download ${asset.key} from remote theme`) } }), ) @@ -160,11 +156,7 @@ export async function deleteRemovedAssets( assetsDeletedFromRemote.map((file) => { if (localFileSystem.files.get(file.key)) { return localFileSystem.delete(file.key).then(() => { - outputInfo( - outputContent`• ${timestampDateFormat.format(new Date())} Synced ${outputToken.raw( - '»', - )} ${outputToken.gray(`remove ${file.key} from local theme`)}`, - ) + outputInfo(`• ${timestampDateFormat.format(new Date())} Synced » remove ${file.key} from local theme`) }) } }), diff --git a/packages/theme/src/cli/utilities/theme-fs.ts b/packages/theme/src/cli/utilities/theme-fs.ts index 3d8df3c97fa..ae68f303f27 100644 --- a/packages/theme/src/cli/utilities/theme-fs.ts +++ b/packages/theme/src/cli/utilities/theme-fs.ts @@ -7,7 +7,7 @@ import {DEFAULT_IGNORE_PATTERNS, timestampDateFormat} from '../constants.js' import {glob, readFile, ReadOptions, fileExists, mkdir, writeFile, removeFile} from '@shopify/cli-kit/node/fs' import {joinPath, basename, relativePath} from '@shopify/cli-kit/node/path' import {lookupMimeType, setMimeTypes} from '@shopify/cli-kit/node/mimes' -import {outputContent, outputDebug, outputInfo, outputToken, outputWarn} from '@shopify/cli-kit/node/output' +import {outputDebug, outputInfo, outputWarn} from '@shopify/cli-kit/node/output' import {buildThemeAsset} from '@shopify/cli-kit/node/themes/factories' import {AdminSession} from '@shopify/cli-kit/node/session' import {bulkUploadThemeAssets, deleteThemeAssets} from '@shopify/cli-kit/node/themes/api' @@ -456,9 +456,7 @@ function dirPath(filePath: string) { } function outputSyncResult(action: 'update' | 'delete', fileKey: string): void { - outputInfo( - outputContent`• ${timestampDateFormat.format(new Date())} Synced ${outputToken.raw('»')} ${action} ${fileKey}`, - ) + outputInfo(`• ${timestampDateFormat.format(new Date())} Synced » ${action} ${fileKey}`) } export function inferLocalHotReloadScriptPath() { diff --git a/packages/theme/src/cli/utilities/theme-store.ts b/packages/theme/src/cli/utilities/theme-store.ts index fe92ef8e9df..b4645ea3fbc 100644 --- a/packages/theme/src/cli/utilities/theme-store.ts +++ b/packages/theme/src/cli/utilities/theme-store.ts @@ -1,19 +1,17 @@ import {themeFlags} from '../flags.js' import {getThemeStore, setThemeStore} from '../services/local-storage.js' import {AbortError} from '@shopify/cli-kit/node/error' -import {outputContent, outputToken} from '@shopify/cli-kit/node/output' export function ensureThemeStore(flags: {store: string | undefined}): string { const store = flags.store || getThemeStore() if (!store) { - throw new AbortError( - 'A store is required', - `Specify the store passing ${ - outputContent`${outputToken.genericShellCommand(`--${themeFlags.store.name}={your_store_url}`)}`.value - } or set the ${ - outputContent`${outputToken.genericShellCommand(themeFlags.store.env as string)}`.value - } environment variable.`, - ) + throw new AbortError('A store is required', [ + 'Specify the store passing ', + {command: `--${themeFlags.store.name}={your_store_url}`}, + ' or set the ', + {command: themeFlags.store.env as string}, + ' environment variable.', + ]) } setThemeStore(store) return store