From 8f1bb98900d5d169c000bdbc0df26057768adcc5 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Thu, 24 Sep 2026 11:46:29 +0200 Subject: [PATCH] Add typed JSON output to theme rename --- .../generated/generated_docs_data_v2.json | 11 +- packages/cli/README.md | 109 +++++++++++- packages/cli/oclif.manifest.json | 11 +- .../rules/json-output-command-exceptions.js | 1 - .../src/cli/commands/theme/rename.test.ts | 166 ++++++++++++++++++ .../theme/src/cli/commands/theme/rename.ts | 27 ++- .../theme/src/cli/services/rename.test.ts | 49 +++++- packages/theme/src/cli/services/rename.ts | 24 +-- .../theme/src/cli/services/rename/result.ts | 24 +++ .../theme/src/cli/services/rename/types.ts | 24 +++ 10 files changed, 422 insertions(+), 24 deletions(-) create mode 100644 packages/theme/src/cli/commands/theme/rename.test.ts create mode 100644 packages/theme/src/cli/services/rename/result.ts create mode 100644 packages/theme/src/cli/services/rename/types.ts diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index de6d0a03447..5cab3081839 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -9046,6 +9046,15 @@ "isOptional": true, "environmentValue": "SHOPIFY_FLAG_ENVIRONMENT" }, + { + "filePath": "docs-shopify.dev/commands/interfaces/theme-rename.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-j, --json", + "value": "''", + "description": "Output the result as JSON. Automatically disables color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_JSON" + }, { "filePath": "docs-shopify.dev/commands/interfaces/theme-rename.interface.ts", "syntaxKind": "PropertySignature", @@ -9083,7 +9092,7 @@ "environmentValue": "SHOPIFY_FLAG_THEME_ID" } ], - "value": "export interface themerename {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * Rename your development theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment '?: string\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Rename your remote live theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\n\n /**\n * The new name for the theme. Required if non interactive.\n * @environment SHOPIFY_FLAG_NEW_NAME\n */\n '-n, --name '?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password '?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * Theme ID or name of the remote theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + "value": "export interface themerename {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias '?: string\n\n /**\n * Rename your development theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment '?: string\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Rename your remote live theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\n\n /**\n * The new name for the theme. Required if non interactive.\n * @environment SHOPIFY_FLAG_NEW_NAME\n */\n '-n, --name '?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password '?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path '?: string\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store '?: string\n\n /**\n * Theme ID or name of the remote theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, "themeshare": { diff --git a/packages/cli/README.md b/packages/cli/README.md index 9df3ee06a96..e5fffbc55bd 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -9337,7 +9337,7 @@ Renames an existing theme. ``` USAGE - $ shopify theme rename [--auth-alias ] [-d] [-e ...] [--json-schema] [-l] [-n ] + $ shopify theme rename [--auth-alias ] [-d] [-e ...] [-j] [--json-schema] [-l] [-n ] [--no-color] [--password ] [--path ] [-s ] [-t ] [--verbose] FLAGS @@ -9349,6 +9349,10 @@ FLAGS The environment to apply to the current command. [env: SHOPIFY_FLAG_ENVIRONMENT] + -j, --json + Output the result as JSON. Automatically disables color output. + [env: SHOPIFY_FLAG_JSON] + -l, --live Rename your remote live theme. Use --development, --live, or --theme in non-interactive environments. [env: SHOPIFY_FLAG_LIVE] @@ -9397,6 +9401,109 @@ DESCRIPTION If no theme is specified, then you're prompted to select the theme that you want to rename from the list of themes in your store. + + + Use `--json-schema` to print the result, error, and event schemas. + + Output from `--json` conforms to the `ThemeRenameResult` schema. + + ```json + { + "anyOf": [ + { + "$ref": "#/definitions/ThemeRenameEnvironment/properties/result" + }, + { + "type": "object", + "properties": { + "environments": { + "type": "array", + "items": { + "$ref": "#/definitions/ThemeRenameEnvironment" + } + } + }, + "required": [ + "environments" + ], + "additionalProperties": false + } + ], + "title": "ThemeRenameResult", + "definitions": { + "RenamedTheme": { + "type": "object", + "properties": { + "id": { + "type": "number" + }, + "name": { + "type": "string" + }, + "role": { + "type": "string" + }, + "processing": { + "type": "boolean" + }, + "createdAtRuntime": { + "type": "boolean" + }, + "src": { + "type": "string" + }, + "shop": { + "type": "string" + } + }, + "required": [ + "id", + "name", + "role", + "processing", + "createdAtRuntime", + "shop" + ], + "additionalProperties": false + }, + "ThemeRenameEnvironment": { + "type": "object", + "properties": { + "environment": { + "type": "string" + }, + "result": { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "success" + }, + "originalName": { + "type": "string" + }, + "theme": { + "$ref": "#/definitions/RenamedTheme" + } + }, + "required": [ + "status", + "originalName", + "theme" + ], + "additionalProperties": false + } + }, + "required": [ + "environment", + "result" + ], + "additionalProperties": false + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + ``` ``` ## `shopify theme share` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 697a10eda70..eaaa4a4665c 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -11560,7 +11560,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "description": "Renames a theme in your store.\n\n If no theme is specified, then you're prompted to select the theme that you want to rename from the list of themes in your store.\n ", + "description": "Renames a theme in your store.\n\n If no theme is specified, then you're prompted to select the theme that you want to rename from the list of themes in your store.\n \n\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeRenameResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeRenameEnvironment/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeRenameEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemeRenameResult\",\n \"definitions\": {\n \"RenamedTheme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\"\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"createdAtRuntime\": {\n \"type\": \"boolean\"\n },\n \"src\": {\n \"type\": \"string\"\n },\n \"shop\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"processing\",\n \"createdAtRuntime\",\n \"shop\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeRenameEnvironment\": {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"success\"\n },\n \"originalName\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/RenamedTheme\"\n }\n },\n \"required\": [\n \"status\",\n \"originalName\",\n \"theme\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "descriptionWithMarkdown": "Renames a theme in your store.\n\n If no theme is specified, then you're prompted to select the theme that you want to rename from the list of themes in your store.\n ", "enableJsonFlag": false, "flags": { @@ -11589,6 +11589,15 @@ "name": "environment", "type": "option" }, + "json": { + "allowNo": false, + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "type": "boolean" + }, "json-schema": { "allowNo": false, "description": "Print the command's JSON schemas.", diff --git a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js index 97a8d0b9ad4..73d9381d6dc 100644 --- a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js +++ b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js @@ -51,7 +51,6 @@ const commandExceptions = [ 'packages/theme/src/cli/commands/theme/profile.ts', 'packages/theme/src/cli/commands/theme/pull.ts', 'packages/theme/src/cli/commands/theme/push.ts', - 'packages/theme/src/cli/commands/theme/rename.ts', 'packages/theme/src/cli/commands/theme/share.ts', // Streaming commands without a single finite result. diff --git a/packages/theme/src/cli/commands/theme/rename.test.ts b/packages/theme/src/cli/commands/theme/rename.test.ts new file mode 100644 index 00000000000..d4229df5b00 --- /dev/null +++ b/packages/theme/src/cli/commands/theme/rename.test.ts @@ -0,0 +1,166 @@ +import Rename from './rename.js' +import {themeRenameJsonOutputSchema} from '../../services/rename/types.js' +import {findOrSelectTheme} from '../../utilities/theme-selector.js' +import {Config} from '@oclif/core' +import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {themeUpdate} from '@shopify/cli-kit/node/themes/api' +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' +import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events' +import {inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs' +import {joinPath} from '@shopify/cli-kit/node/path' +import {outputWarn} from '@shopify/cli-kit/node/output' +import {describe, expect, test, vi} from 'vitest' + +vi.mock('@shopify/cli-kit/node/session') +vi.mock('@shopify/cli-kit/node/themes/api') +vi.mock('@shopify/cli-kit/node/metadata') +vi.mock('../../utilities/theme-selector.js') +vi.mock('../../utilities/theme-store.js', () => ({ensureThemeStore: ({store}: {store: string}) => store})) + +const originalTheme = {id: 1, name: 'Original', role: 'unpublished', processing: false, createdAtRuntime: false} +const renamedTheme = {...originalTheme, name: 'Renamed Theme'} +const store = 'test.myshopify.com' + +async function run(argv: string[]) { + const config = new Config({root: __dirname}) + await config.load() + vi.mocked(ensureAuthenticatedThemes).mockImplementation(async (storeFqdn) => ({token: 'token', storeFqdn})) + return runWithCommandEventsForCommand(argv, () => new Rename(argv, config).run()) +} + +async function inEnvironments(run: () => Promise) { + await inTemporaryDirectory(async (directory) => { + await writeFile( + joinPath(directory, 'shopify.theme.toml'), + ` +[environments.first] +store = "first.myshopify.com" +password = "token" +[environments.second] +store = "second.myshopify.com" +password = "token" +`, + ) + const cwd = vi.spyOn(process, 'cwd').mockReturnValue(directory) + try { + await run() + } finally { + cwd.mockRestore() + } + }) +} + +describe('theme rename JSON output', () => { + test('exposes its schema and JSON flag in help', () => { + expect(Rename.jsonOutputSchema).toBe(themeRenameJsonOutputSchema) + expect(Rename.flags.json).toBeDefined() + expect(Rename.description).toContain('ThemeRenameResult') + }) + + test.each([undefined, '', 'https://example.com/theme.zip'])( + 'returns the updated theme and omits missing src (%s)', + async (src) => { + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) + vi.mocked(themeUpdate).mockResolvedValue({...renamedTheme, src}) + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await run(['--store', store, '--theme', '1', '--name', 'Renamed Theme', '--json']) + expect(JSON.parse(stdout())).toEqual({ + status: 'success', + originalName: 'Original', + theme: {...renamedTheme, ...(src === undefined ? {} : {src}), shop: store}, + }) + expect(stderr()).toBe('') + }) + }, + ) + + test('routes diagnostics to stderr', async () => { + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) + vi.mocked(themeUpdate).mockImplementation(async () => { + outputWarn('Retrying request') + return renamedTheme + }) + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await run(['--store', store, '--theme', '1', '--name', 'Renamed Theme', '--json']) + expect(JSON.parse(stdout()).theme.name).toBe('Renamed Theme') + expect(JSON.parse(stderr())).toMatchObject({type: 'diagnostic', level: 'warning', message: 'Retrying request'}) + }) + }) + + test('leaves execution failures to the shared error handler without writing a result', async () => { + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) + vi.mocked(themeUpdate).mockRejectedValue(new Error('Renameing failed')) + await withCapturedStandardStreams(async ({stdout}) => { + await expect(run(['--store', store, '--theme', '1', '--name', 'Renamed Theme', '--json'])).rejects.toThrow( + 'Renameing failed', + ) + expect(stdout()).toBe('') + }) + }) + + test.each(['none', 'partial', 'all'])('collects one document in configured order (%s failures)', async (failures) => { + vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme) + vi.mocked(themeUpdate).mockImplementation(async (_id, _params, session) => { + if (failures === 'all' || (failures === 'partial' && session.storeFqdn.startsWith('first.'))) { + throw new Error('Renameing failed') + } + return renamedTheme + }) + const exitCode = process.exitCode + await inEnvironments(async () => { + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await run([ + '--environment', + 'first', + '--environment', + 'second', + '--theme', + '1', + '--name', + 'Renamed Theme', + '--json', + ]) + const environments: string[] = [] + if (failures === 'none') environments.push('first') + if (failures !== 'all') environments.push('second') + expect(JSON.parse(stdout())).toEqual({ + environments: environments.map((environment) => ({ + environment, + result: { + status: 'success', + originalName: 'Original', + theme: {...renamedTheme, shop: `${environment}.myshopify.com`}, + }, + })), + }) + if (failures === 'none') { + expect(stderr()).toBe('') + } else { + const errors = stderr() + .trim() + .split('\n') + .map((line) => JSON.parse(line)) + expect(errors).toHaveLength(failures === 'all' ? 2 : 1) + expect(errors[0]).toMatchObject({type: 'diagnostic', level: 'error', code: 'theme-environment-failed'}) + } + expect(process.exitCode).toBe(exitCode) + }) + }) + }) + + test.each([ + {status: 'success', originalName: 'Original', theme: {...renamedTheme, id: '1', shop: store}}, + {status: 'success', originalName: 'Original', theme: {...renamedTheme, shop: null}}, + {status: 'success', originalName: 'Original', theme: {...renamedTheme, src: false, shop: store}}, + { + environments: [ + { + environment: 'first', + result: {status: 'success', originalName: 'Original', theme: {...renamedTheme, role: null, shop: store}}, + }, + ], + }, + ])('rejects malformed results %#', (result) => { + expect(() => themeRenameJsonOutputSchema.validate(result)).toThrow() + }) +}) diff --git a/packages/theme/src/cli/commands/theme/rename.ts b/packages/theme/src/cli/commands/theme/rename.ts index 10f0cb980ce..2b855f38afa 100644 --- a/packages/theme/src/cli/commands/theme/rename.ts +++ b/packages/theme/src/cli/commands/theme/rename.ts @@ -1,12 +1,20 @@ +import {themeRenameJsonOutputSchema} from '../../services/rename/types.js' +import {renderThemeRenameResult} from '../../services/rename/result.js' import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.js' import {themeFlags} from '../../flags.js' -import {RenameOptions, renameTheme} from '../../services/rename.js' +import {renameTheme} from '../../services/rename.js' +import {OutputFlags} from '@oclif/core/interfaces' +import {outputResult} from '@shopify/cli-kit/node/output' import {Flags} from '@oclif/core' -import {globalFlags, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli' +import {globalFlags, jsonFlag, requiredIfNonInteractive} from '@shopify/cli-kit/node/cli' import {AdminSession} from '@shopify/cli-kit/node/session' import type {NonTTYFlagRequirement} from '@shopify/cli-kit/node/base-command' export default class Rename extends ThemeCommand { + static get jsonOutputSchema() { + return themeRenameJsonOutputSchema + } + static summary = 'Renames an existing theme.' static descriptionWithMarkdown = `Renames a theme in your store. @@ -18,6 +26,7 @@ export default class Rename extends ThemeCommand { static flags = { ...globalFlags, + ...jsonFlag, ...themeFlags, name: requiredIfNonInteractive( Flags.string({ @@ -53,7 +62,17 @@ export default class Rename extends ThemeCommand { return [{flags: ['theme', 'development', 'live']}] } - async command(flags: RenameOptions, adminSession: AdminSession) { - await renameTheme(flags, adminSession) + async command(flags: OutputFlags, adminSession: AdminSession, multiEnvironment = false) { + const result = await renameTheme(flags, adminSession) + if (flags.json && multiEnvironment) return result.data + renderThemeRenameResult(result, flags.json ? 'json' : 'text', flags.environment) + } + + protected collectsEnvironmentResults(flags: {json?: boolean}): boolean { + return Boolean(flags.json) + } + + protected renderEnvironmentResults(environments: {environment: string; result: unknown}[]): void { + outputResult(themeRenameJsonOutputSchema.encode(themeRenameJsonOutputSchema.validate({environments}))) } } diff --git a/packages/theme/src/cli/services/rename.test.ts b/packages/theme/src/cli/services/rename.test.ts index 48ee84ba80f..e55d977d09a 100644 --- a/packages/theme/src/cli/services/rename.test.ts +++ b/packages/theme/src/cli/services/rename.test.ts @@ -1,4 +1,5 @@ -import {RenameOptions, renameTheme} from './rename.js' +import {RenameOptions, renameTheme as executeRename} from './rename.js' +import {renderThemeRenameResult} from './rename/result.js' import {findOrSelectTheme} from '../utilities/theme-selector.js' import {Theme} from '@shopify/cli-kit/node/themes/types' import {test, describe, expect, vi} from 'vitest' @@ -80,3 +81,49 @@ describe('renameTheme', () => { }) }) }) + +async function renameTheme(...args: Parameters) { + const result = await executeRename(...args) + renderThemeRenameResult(result, 'text') + return result +} + +test('returns updated API data without presenting a final result', async () => { + const updatedTheme = { + ...developmentTheme, + name: 'Canonical name', + role: 'development', + processing: false, + createdAtRuntime: false, + } + vi.mocked(findOrSelectTheme).mockResolvedValue(developmentTheme) + vi.mocked(themeUpdate).mockResolvedValue(updatedTheme) + const result = await executeRename(options, adminSession) + expect(result).toEqual({ + data: { + status: 'success', + originalName: developmentTheme.name, + theme: {...updatedTheme, shop: adminSession.storeFqdn}, + }, + originalTheme: developmentTheme, + requestedName: 'Renamed Theme', + }) + expect(renderSuccess).not.toHaveBeenCalled() +}) + +test('preserves the requested name and environment label in terminal output', async () => { + vi.mocked(findOrSelectTheme).mockResolvedValue(developmentTheme) + vi.mocked(themeUpdate).mockResolvedValue({...developmentTheme, name: 'Canonical name'}) + const result = await executeRename(options, adminSession) + renderThemeRenameResult(result, 'text', ['staging']) + expect(renderSuccess).toHaveBeenCalledWith({ + body: [ + {subdued: 'Environment: staging\n\n'}, + 'The theme', + "'my development theme'", + {subdued: '(#1)'}, + 'was renamed to', + "'Renamed Theme'", + ], + }) +}) diff --git a/packages/theme/src/cli/services/rename.ts b/packages/theme/src/cli/services/rename.ts index 4d487367552..6fb252eb308 100644 --- a/packages/theme/src/cli/services/rename.ts +++ b/packages/theme/src/cli/services/rename.ts @@ -1,8 +1,7 @@ -import {themeComponent} from '../utilities/theme-ui.js' +import {ThemeRenameResult} from './rename/types.js' import {findOrSelectTheme} from '../utilities/theme-selector.js' import {themeUpdate} from '@shopify/cli-kit/node/themes/api' import {AdminSession} from '@shopify/cli-kit/node/session' -import {renderSuccess} from '@shopify/cli-kit/node/ui' import {promptThemeName} from '@shopify/cli-kit/node/themes/utils' export interface RenameOptions { @@ -10,10 +9,9 @@ export interface RenameOptions { development?: boolean theme?: string live?: boolean - environment?: string } -export async function renameTheme(options: RenameOptions, adminSession: AdminSession) { +export async function renameTheme(options: RenameOptions, adminSession: AdminSession): Promise { const newName = options.name ?? (await promptThemeName('New name for the theme')) const theme = await findOrSelectTheme(adminSession, { @@ -25,15 +23,11 @@ export async function renameTheme(options: RenameOptions, adminSession: AdminSes }, }) - await themeUpdate(theme.id, {name: newName}, adminSession) - - renderSuccess({ - body: [ - ...(options.environment ? [{subdued: `Environment: ${options.environment}\n\n`}] : []), - 'The theme', - ...themeComponent(theme), - 'was renamed to', - `'${newName}'`, - ], - }) + // The API helper rejects missing themes and user errors before returning. + const renamedTheme = (await themeUpdate(theme.id, {name: newName}, adminSession))! + return { + data: {status: 'success', originalName: theme.name, theme: {...renamedTheme, shop: adminSession.storeFqdn}}, + originalTheme: theme, + requestedName: newName, + } } diff --git a/packages/theme/src/cli/services/rename/result.ts b/packages/theme/src/cli/services/rename/result.ts new file mode 100644 index 00000000000..fc286110072 --- /dev/null +++ b/packages/theme/src/cli/services/rename/result.ts @@ -0,0 +1,24 @@ +import {themeRenameJsonOutputSchema, type ThemeRenameResult} from './types.js' +import {themeComponent} from '../../utilities/theme-ui.js' +import {outputResult} from '@shopify/cli-kit/node/output' +import {renderSuccess} from '@shopify/cli-kit/node/ui' + +export function renderThemeRenameResult( + result: ThemeRenameResult, + format: 'text' | 'json', + environment?: string[], +): void { + if (format === 'json') { + outputResult(themeRenameJsonOutputSchema.encode(result.data)) + return + } + renderSuccess({ + body: [ + ...(environment ? [{subdued: `Environment: ${environment}\n\n`}] : []), + 'The theme', + ...themeComponent(result.originalTheme), + 'was renamed to', + `'${result.requestedName}'`, + ], + }) +} diff --git a/packages/theme/src/cli/services/rename/types.ts b/packages/theme/src/cli/services/rename/types.ts new file mode 100644 index 00000000000..ad27e9b0591 --- /dev/null +++ b/packages/theme/src/cli/services/rename/types.ts @@ -0,0 +1,24 @@ +import {ThemeMutationSuccessSchema} from '../theme-mutation/status.js' +import {ThemeMutationThemeSchema} from '../theme-mutation/types.js' +import {defineJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' +import type {Theme} from '@shopify/cli-kit/node/themes/types' + +const ThemeRenameResultSchema = ThemeMutationSuccessSchema.extend({ + originalName: zod.string(), + theme: ThemeMutationThemeSchema, +}) +const ThemeRenameEnvironmentSchema = zod.object({environment: zod.string(), result: ThemeRenameResultSchema}) + +export const themeRenameJsonOutputSchema = defineJsonOutputSchema({ + name: 'ThemeRenameResult', + schema: zod.union([ThemeRenameResultSchema, zod.object({environments: zod.array(ThemeRenameEnvironmentSchema)})]), + definitions: {RenamedTheme: ThemeMutationThemeSchema, ThemeRenameEnvironment: ThemeRenameEnvironmentSchema}, +}) + +export type ThemeRenameData = zod.infer +export interface ThemeRenameResult { + data: ThemeRenameData + originalTheme: Theme + requestedName: string +}