Skip to content

Commit da398e8

Browse files
Add typed JSON output to theme share
1 parent 1fefc12 commit da398e8

10 files changed

Lines changed: 313 additions & 13 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@shopify/cli": minor
3+
---
4+
5+
Add typed JSON output to theme share with theme details and preview links.

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8590,6 +8590,15 @@
85908590
"isOptional": true,
85918591
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
85928592
},
8593+
{
8594+
"filePath": "docs-shopify.dev/commands/interfaces/theme-share.interface.ts",
8595+
"syntaxKind": "PropertySignature",
8596+
"name": "-j, --json",
8597+
"value": "''",
8598+
"description": "Output the result as JSON. Automatically disables color output.",
8599+
"isOptional": true,
8600+
"environmentValue": "SHOPIFY_FLAG_JSON"
8601+
},
85938602
{
85948603
"filePath": "docs-shopify.dev/commands/interfaces/theme-share.interface.ts",
85958604
"syntaxKind": "PropertySignature",
@@ -8600,7 +8609,7 @@
86008609
"environmentValue": "SHOPIFY_FLAG_STORE"
86018610
}
86028611
],
8603-
"value": "export interface themeshare {\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 * 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 * The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.\n * @environment SHOPIFY_FLAG_LISTING\n */\n '--listing <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 * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
8612+
"value": "export interface themeshare {\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 * 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 * The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.\n * @environment SHOPIFY_FLAG_LISTING\n */\n '--listing <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 * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
86048613
}
86058614
},
86068615
"upgrade": {

‎packages/cli/README.md‎

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8605,14 +8605,18 @@ Creates a shareable, unpublished, and new theme on your theme library with a ran
86058605
86068606
```
86078607
USAGE
8608-
$ shopify theme share [--auth-alias <value>] [-e <value>...] [--json-schema] [--listing <value>] [--no-color]
8609-
[--password <value>] [--path <value>] [-s <value>] [--verbose]
8608+
$ shopify theme share [--auth-alias <value>] [-e <value>...] [-j] [--json-schema] [--listing <value>]
8609+
[--no-color] [--password <value>] [--path <value>] [-s <value>] [--verbose]
86108610

86118611
FLAGS
86128612
-e, --environment=<value>...
86138613
The environment to apply to the current command.
86148614
[env: SHOPIFY_FLAG_ENVIRONMENT]
86158615

8616+
-j, --json
8617+
Output the result as JSON. Automatically disables color output.
8618+
[env: SHOPIFY_FLAG_JSON]
8619+
86168620
-s, --store=<value>
86178621
Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com,
86188622
https://example.myshopify.com).
@@ -8654,6 +8658,103 @@ DESCRIPTION
86548658
This command returns a "preview link"
86558659
(https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can
86568660
share with others.
8661+
8662+
Use `--json` for theme details and links. With multiple environments, the result is an array of successful results in
8663+
requested order, each with its environment name. Failed or skipped environments are reported on stderr and omitted; if
8664+
none succeed, the result is an empty array. Existing exit behavior is unchanged.
8665+
8666+
Output from `--json` conforms to the `ThemeShareResult` schema.
8667+
8668+
Use `--json-schema` to print the result, error, and event schemas.
8669+
8670+
```json
8671+
{
8672+
"anyOf": [
8673+
{
8674+
"type": "object",
8675+
"properties": {
8676+
"environment": {
8677+
"type": "string"
8678+
},
8679+
"theme": {
8680+
"$ref": "#/definitions/ThemePushTheme"
8681+
}
8682+
},
8683+
"required": [
8684+
"theme"
8685+
],
8686+
"additionalProperties": false
8687+
},
8688+
{
8689+
"type": "array",
8690+
"items": {
8691+
"type": "object",
8692+
"properties": {
8693+
"environment": {
8694+
"type": "string"
8695+
},
8696+
"theme": {
8697+
"$ref": "#/definitions/ThemePushTheme"
8698+
}
8699+
},
8700+
"required": [
8701+
"environment",
8702+
"theme"
8703+
],
8704+
"additionalProperties": false
8705+
}
8706+
}
8707+
],
8708+
"title": "ThemeShareResult",
8709+
"definitions": {
8710+
"ThemePushTheme": {
8711+
"type": "object",
8712+
"properties": {
8713+
"id": {
8714+
"type": "number"
8715+
},
8716+
"name": {
8717+
"type": "string"
8718+
},
8719+
"role": {
8720+
"type": "string"
8721+
},
8722+
"shop": {
8723+
"type": "string"
8724+
},
8725+
"editor_url": {
8726+
"type": "string"
8727+
},
8728+
"preview_url": {
8729+
"type": "string"
8730+
},
8731+
"warning": {
8732+
"type": "string"
8733+
},
8734+
"errors": {
8735+
"type": "object",
8736+
"additionalProperties": {
8737+
"type": "array",
8738+
"items": {
8739+
"type": "string"
8740+
}
8741+
}
8742+
}
8743+
},
8744+
"required": [
8745+
"id",
8746+
"name",
8747+
"role",
8748+
"shop",
8749+
"editor_url",
8750+
"preview_url"
8751+
],
8752+
"additionalProperties": false
8753+
}
8754+
},
8755+
"$schema": "http://json-schema.org/draft-07/schema#"
8756+
}
8757+
```
86578758
```
86588759
86598760
## `shopify upgrade`

‎packages/cli/oclif.manifest.json‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11663,8 +11663,8 @@
1166311663
"args": {
1166411664
},
1166511665
"customPluginName": "@shopify/theme",
11666-
"description": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.",
11667-
"descriptionWithMarkdown": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.",
11666+
"description": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n Use `--json` for theme details and links. With multiple environments, the result is an array of successful results in requested order, each with its environment name. Failed or skipped environments are reported on stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.\n\nOutput from `--json` conforms to the `ThemeShareResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePushTheme\"\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePushTheme\"\n }\n },\n \"required\": [\n \"environment\",\n \"theme\"\n ],\n \"additionalProperties\": false\n }\n }\n ],\n \"title\": \"ThemeShareResult\",\n \"definitions\": {\n \"ThemePushTheme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\"\n },\n \"shop\": {\n \"type\": \"string\"\n },\n \"editor_url\": {\n \"type\": \"string\"\n },\n \"preview_url\": {\n \"type\": \"string\"\n },\n \"warning\": {\n \"type\": \"string\"\n },\n \"errors\": {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"shop\",\n \"editor_url\",\n \"preview_url\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
11667+
"descriptionWithMarkdown": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\n Use `--json` for theme details and links. With multiple environments, the result is an array of successful results in requested order, each with its environment name. Failed or skipped environments are reported on stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.",
1166811668
"enableJsonFlag": false,
1166911669
"flags": {
1167011670
"auth-alias": {
@@ -11693,6 +11693,15 @@
1169311693
"name": "force",
1169411694
"type": "boolean"
1169511695
},
11696+
"json": {
11697+
"allowNo": false,
11698+
"char": "j",
11699+
"description": "Output the result as JSON. Automatically disables color output.",
11700+
"env": "SHOPIFY_FLAG_JSON",
11701+
"hidden": false,
11702+
"name": "json",
11703+
"type": "boolean"
11704+
},
1169611705
"json-schema": {
1169711706
"allowNo": false,
1169811707
"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
@@ -73,7 +73,6 @@ const commandExceptions = [
7373
'packages/theme/src/cli/commands/theme/profile.ts',
7474
'packages/theme/src/cli/commands/theme/publish.ts',
7575
'packages/theme/src/cli/commands/theme/rename.ts',
76-
'packages/theme/src/cli/commands/theme/share.ts',
7776

7877
// Streaming commands without a single finite result.
7978
'packages/app/src/cli/commands/app/demo/watcher.ts',
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
import Share from './share.js'
2+
import {executeThemePush} from '../../services/push.js'
3+
import {themeShareJsonOutputSchema} from '../../services/share/types.js'
4+
import {captureStandardStreams} from '../../utilities/test-helpers/stdio.js'
5+
import {Config} from '@oclif/core'
6+
import {getRandomName} from '@shopify/cli-kit/common/string'
7+
import {mockAndCaptureOutput} from '@shopify/cli-kit/node/testing/output'
8+
import {describe, expect, test, vi} from 'vitest'
9+
10+
vi.mock('../../services/push.js')
11+
vi.mock('@shopify/cli-kit/common/string', async (importOriginal) => ({
12+
...(await importOriginal<typeof import('@shopify/cli-kit/common/string')>()),
13+
getRandomName: vi.fn(),
14+
}))
15+
16+
const session = {storeFqdn: 'test.myshopify.com', token: 'token'}
17+
18+
function result() {
19+
return {
20+
theme: {
21+
id: 1,
22+
name: 'Creative Theme',
23+
role: 'unpublished',
24+
shop: session.storeFqdn,
25+
editor_url: 'editor',
26+
preview_url: 'preview',
27+
},
28+
published: false,
29+
hasErrors: false,
30+
errors: {},
31+
}
32+
}
33+
34+
describe('theme share', () => {
35+
test('exposes JSON and schema flags and documents its own result', () => {
36+
expect(Share.jsonOutputSchema).toBe(themeShareJsonOutputSchema)
37+
expect(Share.description).toContain('ThemeShareResult')
38+
expect(Share.flags.json).toBeDefined()
39+
expect(Share.baseFlags).toHaveProperty('json-schema')
40+
})
41+
42+
test('creates an unpublished theme with a random name and forwards the listing', async () => {
43+
vi.mocked(getRandomName).mockReturnValue('Creative Theme')
44+
vi.mocked(executeThemePush).mockResolvedValue(result())
45+
const command = new Share([], new Config({root: '.'}))
46+
const streams = await captureStandardStreams(() =>
47+
command.command({json: true, path: '/theme', listing: 'preset'} as never, session, false),
48+
)
49+
expect(getRandomName).toHaveBeenCalledWith('creative')
50+
expect(executeThemePush).toHaveBeenCalledWith(
51+
expect.objectContaining({unpublished: true, theme: 'Creative Theme', path: '/theme', listing: 'preset'}),
52+
session,
53+
false,
54+
undefined,
55+
)
56+
expect(streams.stdout).toBe(`${themeShareJsonOutputSchema.encode({theme: result().theme})}\n`)
57+
expect(streams.stderr).toBe('')
58+
})
59+
60+
test('keeps upload errors and environment in JSON', async () => {
61+
vi.mocked(executeThemePush).mockResolvedValue({
62+
...result(),
63+
environment: 'staging',
64+
hasErrors: true,
65+
errors: {'assets/theme.css': ['bad CSS']},
66+
})
67+
const command = new Share([], new Config({root: '.'}))
68+
const streams = await captureStandardStreams(() =>
69+
command.command({json: true, environment: ['staging']} as never, session, false),
70+
)
71+
expect(JSON.parse(streams.stdout)).toEqual({
72+
environment: 'staging',
73+
theme: {
74+
...result().theme,
75+
warning: "[staging] The theme 'Creative Theme' was pushed with errors",
76+
errors: {'assets/theme.css': ['bad CSS']},
77+
},
78+
})
79+
})
80+
81+
test('preserves the push success banner in text mode', async () => {
82+
vi.mocked(executeThemePush).mockResolvedValue(result())
83+
const output = mockAndCaptureOutput()
84+
output.clear()
85+
await new Share([], new Config({root: '.'})).command({} as never, session, false)
86+
expect(output.info()).toContain("The theme 'Creative Theme' (#1) was pushed successfully.")
87+
expect(output.info()).toContain('View your theme')
88+
expect(output.info()).toContain('Customize your theme at the theme editor')
89+
})
90+
91+
test('does not emit a result when cancelled', async () => {
92+
vi.mocked(executeThemePush).mockResolvedValue(undefined)
93+
const streams = await captureStandardStreams(() =>
94+
new Share([], new Config({root: '.'})).command({json: true} as never, session, false),
95+
)
96+
expect(streams.stdout).toBe('')
97+
})
98+
99+
test('propagates theme creation failures through the shared error path', async () => {
100+
const failure = new Error('theme creation failed')
101+
vi.mocked(executeThemePush).mockRejectedValue(failure)
102+
const streams = await captureStandardStreams(async () => {
103+
await expect(new Share([], new Config({root: '.'})).command({json: true} as never, session, false)).rejects.toBe(
104+
failure,
105+
)
106+
})
107+
expect(streams.stdout).toBe('')
108+
})
109+
110+
test.each([{id: '1'}, {role: null}, {errors: {file: 'invalid'}}])('rejects malformed theme data %j', (fields) => {
111+
expect(() => themeShareJsonOutputSchema.validate({theme: {...result().theme, ...fields}})).toThrow()
112+
})
113+
})

‎packages/theme/src/cli/commands/theme/share.ts‎

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import {themeShareJsonOutputSchema} from '../../services/share/types.js'
2+
import {renderThemeShareResult, renderThemeShareEnvironmentResults} from '../../services/share/result.js'
13
import {themeFlags} from '../../flags.js'
24
import ThemeCommand from '../../utilities/theme-command.js'
3-
import {push, PushFlags} from '../../services/push.js'
5+
import {executeThemePush, PushFlags} from '../../services/push.js'
46
import {Flags} from '@oclif/core'
5-
import {globalFlags} from '@shopify/cli-kit/node/cli'
7+
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
68
import {getRandomName} from '@shopify/cli-kit/common/string'
79
import {recordTiming} from '@shopify/cli-kit/node/analytics'
810
import {InferredFlags} from '@oclif/core/interfaces'
@@ -13,16 +15,23 @@ import {Writable} from 'stream'
1315

1416
type ShareFlags = InferredFlags<typeof Share.flags>
1517
export default class Share extends ThemeCommand {
18+
static get jsonOutputSchema() {
19+
return themeShareJsonOutputSchema
20+
}
21+
1622
static summary = 'Creates a shareable, unpublished, and new theme on your theme library with a randomized name.'
1723

1824
static descriptionWithMarkdown = `Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.
1925
20-
This command returns a [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.`
26+
This command returns a [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.
27+
28+
Use \`--json\` for theme details and links. With multiple environments, the result is an array of successful results in requested order, each with its environment name. Failed or skipped environments are reported on stderr and omitted; if none succeed, the result is an empty array. Existing exit behavior is unchanged.`
2129

2230
static description = this.descriptionForHelp()
2331

2432
static flags = {
2533
...globalFlags,
34+
...jsonFlag,
2635
...themeFlags,
2736
force: Flags.boolean({
2837
hidden: true,
@@ -59,7 +68,17 @@ export default class Share extends ThemeCommand {
5968
}
6069

6170
recordTiming('theme-command:share')
62-
await push(pushFlags, adminSession, multiEnvironment, context)
71+
const result = await executeThemePush(pushFlags, adminSession, multiEnvironment, context)
72+
if (result && !(flags.json && multiEnvironment)) renderThemeShareResult(result, flags.json ? 'json' : 'text')
6373
recordTiming('theme-command:share')
74+
return result
75+
}
76+
77+
protected collectsEnvironmentResults(flags: {json?: boolean}): boolean {
78+
return Boolean(flags.json)
79+
}
80+
81+
protected renderEnvironmentResults(results: {environment: string; result: unknown}[]): void {
82+
renderThemeShareEnvironmentResults(results)
6483
}
6584
}

0 commit comments

Comments
 (0)