Skip to content

Commit 7336e0a

Browse files
Add typed JSON output to theme rename
1 parent bcdcb7d commit 7336e0a

11 files changed

Lines changed: 396 additions & 25 deletions

File tree

‎.changeset/theme-rename-json.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
'@shopify/cli': minor
3+
---
4+
Add typed JSON output to `theme rename`, including results from multiple environments.

‎docs-shopify.dev/generated/generated_docs_data_v2.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8489,6 +8489,15 @@
84898489
"isOptional": true,
84908490
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
84918491
},
8492+
{
8493+
"filePath": "docs-shopify.dev/commands/interfaces/theme-rename.interface.ts",
8494+
"syntaxKind": "PropertySignature",
8495+
"name": "-j, --json",
8496+
"value": "''",
8497+
"description": "Output the result as JSON. Automatically disables color output.",
8498+
"isOptional": true,
8499+
"environmentValue": "SHOPIFY_FLAG_JSON"
8500+
},
84928501
{
84938502
"filePath": "docs-shopify.dev/commands/interfaces/theme-rename.interface.ts",
84948503
"syntaxKind": "PropertySignature",
@@ -8526,7 +8535,7 @@
85268535
"environmentValue": "SHOPIFY_FLAG_THEME_ID"
85278536
}
85288537
],
8529-
"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 <value>'?: 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 <value>'?: 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 <value>'?: 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 <value>'?: 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 <value>'?: 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 <value>'?: 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 <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
8538+
"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 <value>'?: 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 <value>'?: 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 <value>'?: 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 <value>'?: 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 <value>'?: 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 <value>'?: 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 <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
85308539
}
85318540
},
85328541
"themeshare": {

‎packages/cli/README.md‎

Lines changed: 101 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8862,7 +8862,7 @@ Renames an existing theme.
88628862

88638863
```
88648864
USAGE
8865-
$ shopify theme rename [--auth-alias <value>] [-d] [-e <value>...] [--json-schema] [-l] [-n <value>]
8865+
$ shopify theme rename [--auth-alias <value>] [-d] [-e <value>...] [-j] [--json-schema] [-l] [-n <value>]
88668866
[--no-color] [--password <value>] [--path <value>] [-s <value>] [-t <value>] [--verbose]
88678867
88688868
FLAGS
@@ -8874,6 +8874,10 @@ FLAGS
88748874
The environment to apply to the current command.
88758875
[env: SHOPIFY_FLAG_ENVIRONMENT]
88768876
8877+
-j, --json
8878+
Output the result as JSON. Automatically disables color output.
8879+
[env: SHOPIFY_FLAG_JSON]
8880+
88778881
-l, --live
88788882
Rename your remote live theme. Use --development, --live, or --theme in non-interactive environments.
88798883
[env: SHOPIFY_FLAG_LIVE]
@@ -8922,6 +8926,102 @@ DESCRIPTION
89228926
89238927
If no theme is specified, then you're prompted to select the theme that you want to rename from the list of themes in
89248928
your store.
8929+
With `--json`, returns the renamed theme. Multiple environments return one document in configured order, omitting
8930+
failed environments and reporting their errors on stderr. Exit behavior is unchanged.
8931+
8932+
8933+
Output from `--json` conforms to the `ThemeRenameResult` schema.
8934+
8935+
Use `--json-schema` to print the result, error, and event schemas.
8936+
8937+
```json
8938+
{
8939+
"anyOf": [
8940+
{
8941+
"$ref": "#/definitions/ThemeRenameEnvironment/properties/result"
8942+
},
8943+
{
8944+
"type": "object",
8945+
"properties": {
8946+
"environments": {
8947+
"type": "array",
8948+
"items": {
8949+
"$ref": "#/definitions/ThemeRenameEnvironment"
8950+
}
8951+
}
8952+
},
8953+
"required": [
8954+
"environments"
8955+
],
8956+
"additionalProperties": false
8957+
}
8958+
],
8959+
"title": "ThemeRenameResult",
8960+
"definitions": {
8961+
"RenamedTheme": {
8962+
"type": "object",
8963+
"properties": {
8964+
"id": {
8965+
"type": "number"
8966+
},
8967+
"name": {
8968+
"type": "string"
8969+
},
8970+
"role": {
8971+
"type": "string"
8972+
},
8973+
"processing": {
8974+
"type": "boolean"
8975+
},
8976+
"createdAtRuntime": {
8977+
"type": "boolean"
8978+
},
8979+
"src": {
8980+
"type": "string"
8981+
},
8982+
"shop": {
8983+
"type": "string"
8984+
}
8985+
},
8986+
"required": [
8987+
"id",
8988+
"name",
8989+
"role",
8990+
"processing",
8991+
"createdAtRuntime",
8992+
"shop"
8993+
],
8994+
"additionalProperties": false
8995+
},
8996+
"ThemeRenameEnvironment": {
8997+
"type": "object",
8998+
"properties": {
8999+
"environment": {
9000+
"type": "string"
9001+
},
9002+
"result": {
9003+
"type": "object",
9004+
"properties": {
9005+
"theme": {
9006+
"$ref": "#/definitions/RenamedTheme"
9007+
}
9008+
},
9009+
"required": [
9010+
"theme"
9011+
],
9012+
"additionalProperties": false
9013+
}
9014+
},
9015+
"required": [
9016+
"environment",
9017+
"result"
9018+
],
9019+
"additionalProperties": false
9020+
}
9021+
},
9022+
"$schema": "http://json-schema.org/draft-07/schema#"
9023+
}
9024+
```
89259025
```
89269026
89279027
## `shopify theme share`

‎packages/cli/oclif.manifest.json‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11551,8 +11551,8 @@
1155111551
"args": {
1155211552
},
1155311553
"customPluginName": "@shopify/theme",
11554-
"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 ",
11555-
"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 ",
11554+
"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 With `--json`, returns the renamed theme. Multiple environments return one document in configured order, omitting failed environments and reporting their errors on stderr. Exit behavior is unchanged.\n \n\nOutput from `--json` conforms to the `ThemeRenameResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\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 \"theme\": {\n \"$ref\": \"#/definitions/RenamedTheme\"\n }\n },\n \"required\": [\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```",
11555+
"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 With `--json`, returns the renamed theme. Multiple environments return one document in configured order, omitting failed environments and reporting their errors on stderr. Exit behavior is unchanged.\n ",
1155611556
"enableJsonFlag": false,
1155711557
"flags": {
1155811558
"auth-alias": {
@@ -11580,6 +11580,15 @@
1158011580
"name": "environment",
1158111581
"type": "option"
1158211582
},
11583+
"json": {
11584+
"allowNo": false,
11585+
"char": "j",
11586+
"description": "Output the result as JSON. Automatically disables color output.",
11587+
"env": "SHOPIFY_FLAG_JSON",
11588+
"hidden": false,
11589+
"name": "json",
11590+
"type": "boolean"
11591+
},
1158311592
"json-schema": {
1158411593
"allowNo": false,
1158511594
"description": "Print the command's JSON schemas.",

‎packages/eslint-plugin-cli/rules/json-output-command-exceptions.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ const commandExceptions = [
6868
'packages/theme/src/cli/commands/theme/profile.ts',
6969
'packages/theme/src/cli/commands/theme/pull.ts',
7070
'packages/theme/src/cli/commands/theme/push.ts',
71-
'packages/theme/src/cli/commands/theme/rename.ts',
7271
'packages/theme/src/cli/commands/theme/share.ts',
7372

7473
// Streaming commands without a single finite result.
Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
import Rename from './rename.js'
2+
import {themeRenameJsonOutputSchema} from '../../services/rename/types.js'
3+
import {findOrSelectTheme} from '../../utilities/theme-selector.js'
4+
import {Config} from '@oclif/core'
5+
import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
6+
import {themeUpdate} from '@shopify/cli-kit/node/themes/api'
7+
import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output'
8+
import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events'
9+
import {inTemporaryDirectory, writeFile} from '@shopify/cli-kit/node/fs'
10+
import {joinPath} from '@shopify/cli-kit/node/path'
11+
import {outputWarn} from '@shopify/cli-kit/node/output'
12+
import {describe, expect, test, vi} from 'vitest'
13+
14+
vi.mock('@shopify/cli-kit/node/session')
15+
vi.mock('@shopify/cli-kit/node/themes/api')
16+
vi.mock('@shopify/cli-kit/node/metadata')
17+
vi.mock('../../utilities/theme-selector.js')
18+
vi.mock('../../utilities/theme-store.js', () => ({ensureThemeStore: ({store}: {store: string}) => store}))
19+
20+
const originalTheme = {id: 1, name: 'Original', role: 'unpublished', processing: false, createdAtRuntime: false}
21+
const renamedTheme = {...originalTheme, name: 'Renamed Theme'}
22+
const store = 'test.myshopify.com'
23+
24+
async function run(argv: string[]) {
25+
const config = new Config({root: __dirname})
26+
await config.load()
27+
vi.mocked(ensureAuthenticatedThemes).mockImplementation(async (storeFqdn) => ({token: 'token', storeFqdn}))
28+
return runWithCommandEventsForCommand(argv, () => new Rename(argv, config).run())
29+
}
30+
31+
async function inEnvironments(run: () => Promise<void>) {
32+
await inTemporaryDirectory(async (directory) => {
33+
await writeFile(
34+
joinPath(directory, 'shopify.theme.toml'),
35+
`
36+
[environments.first]
37+
store = "first.myshopify.com"
38+
password = "token"
39+
[environments.second]
40+
store = "second.myshopify.com"
41+
password = "token"
42+
`,
43+
)
44+
const cwd = vi.spyOn(process, 'cwd').mockReturnValue(directory)
45+
try {
46+
await run()
47+
} finally {
48+
cwd.mockRestore()
49+
}
50+
})
51+
}
52+
53+
describe('theme rename JSON output', () => {
54+
test('exposes its schema and JSON flag in help', () => {
55+
expect(Rename.jsonOutputSchema).toBe(themeRenameJsonOutputSchema)
56+
expect(Rename.flags.json).toBeDefined()
57+
expect(Rename.description).toContain('ThemeRenameResult')
58+
})
59+
60+
test.each([undefined, '', 'https://example.com/theme.zip'])(
61+
'returns the updated theme and omits missing src (%s)',
62+
async (src) => {
63+
vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme)
64+
vi.mocked(themeUpdate).mockResolvedValue({...renamedTheme, src})
65+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
66+
await run(['--store', store, '--theme', '1', '--name', 'Renamed Theme', '--json'])
67+
expect(JSON.parse(stdout())).toEqual({
68+
theme: {...renamedTheme, ...(src === undefined ? {} : {src}), shop: store},
69+
})
70+
expect(stderr()).toBe('')
71+
})
72+
},
73+
)
74+
75+
test('routes diagnostics to stderr', async () => {
76+
vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme)
77+
vi.mocked(themeUpdate).mockImplementation(async () => {
78+
outputWarn('Retrying request')
79+
return renamedTheme
80+
})
81+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
82+
await run(['--store', store, '--theme', '1', '--name', 'Renamed Theme', '--json'])
83+
expect(JSON.parse(stdout()).theme.name).toBe('Renamed Theme')
84+
expect(JSON.parse(stderr())).toMatchObject({type: 'diagnostic', level: 'warning', message: 'Retrying request'})
85+
})
86+
})
87+
88+
test('leaves execution failures to the shared error handler without writing a result', async () => {
89+
vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme)
90+
vi.mocked(themeUpdate).mockRejectedValue(new Error('Renameing failed'))
91+
await withCapturedStandardStreams(async ({stdout}) => {
92+
await expect(run(['--store', store, '--theme', '1', '--name', 'Renamed Theme', '--json'])).rejects.toThrow(
93+
'Renameing failed',
94+
)
95+
expect(stdout()).toBe('')
96+
})
97+
})
98+
99+
test.each(['none', 'partial', 'all'])('collects one document in configured order (%s failures)', async (failures) => {
100+
vi.mocked(findOrSelectTheme).mockResolvedValue(originalTheme)
101+
vi.mocked(themeUpdate).mockImplementation(async (_id, _params, session) => {
102+
if (failures === 'all' || (failures === 'partial' && session.storeFqdn.startsWith('first.'))) {
103+
throw new Error('Renameing failed')
104+
}
105+
return renamedTheme
106+
})
107+
const exitCode = process.exitCode
108+
await inEnvironments(async () => {
109+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
110+
await run([
111+
'--environment',
112+
'first',
113+
'--environment',
114+
'second',
115+
'--theme',
116+
'1',
117+
'--name',
118+
'Renamed Theme',
119+
'--json',
120+
])
121+
const environments: string[] = []
122+
if (failures === 'none') environments.push('first')
123+
if (failures !== 'all') environments.push('second')
124+
expect(JSON.parse(stdout())).toEqual({
125+
environments: environments.map((environment) => ({
126+
environment,
127+
result: {theme: {...renamedTheme, shop: `${environment}.myshopify.com`}},
128+
})),
129+
})
130+
if (failures === 'none') {
131+
expect(stderr()).toBe('')
132+
} else {
133+
const errors = stderr()
134+
.trim()
135+
.split('\n')
136+
.map((line) => JSON.parse(line))
137+
expect(errors).toHaveLength(failures === 'all' ? 2 : 1)
138+
expect(errors[0]).toMatchObject({type: 'diagnostic', level: 'error', code: 'theme-environment-failed'})
139+
}
140+
expect(process.exitCode).toBe(exitCode)
141+
})
142+
})
143+
})
144+
145+
test.each([
146+
{theme: {...renamedTheme, id: '1', shop: store}},
147+
{theme: {...renamedTheme, shop: null}},
148+
{theme: {...renamedTheme, src: false, shop: store}},
149+
{environments: [{environment: 'first', result: {theme: {...renamedTheme, role: null, shop: store}}}]},
150+
])('rejects malformed results %#', (result) => {
151+
expect(() => themeRenameJsonOutputSchema.validate(result)).toThrow()
152+
})
153+
})

0 commit comments

Comments
 (0)