Skip to content

Commit e07dbbd

Browse files
Add typed JSON output to theme share
1 parent d7b6919 commit e07dbbd

9 files changed

Lines changed: 318 additions & 11 deletions

File tree

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9174,6 +9174,15 @@
91749174
"isOptional": true,
91759175
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
91769176
},
9177+
{
9178+
"filePath": "docs-shopify.dev/commands/interfaces/theme-share.interface.ts",
9179+
"syntaxKind": "PropertySignature",
9180+
"name": "-j, --json",
9181+
"value": "''",
9182+
"description": "Output the result as JSON. Automatically disables color output.",
9183+
"isOptional": true,
9184+
"environmentValue": "SHOPIFY_FLAG_JSON"
9185+
},
91779186
{
91789187
"filePath": "docs-shopify.dev/commands/interfaces/theme-share.interface.ts",
91799188
"syntaxKind": "PropertySignature",
@@ -9184,7 +9193,7 @@
91849193
"environmentValue": "SHOPIFY_FLAG_STORE"
91859194
}
91869195
],
9187-
"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}"
9196+
"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}"
91889197
}
91899198
},
91909199
"upgrade": {

‎packages/cli/README.md‎

Lines changed: 111 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9714,14 +9714,18 @@ Creates a shareable, unpublished, and new theme on your theme library with a ran
97149714
97159715
```
97169716
USAGE
9717-
$ shopify theme share [--auth-alias <value>] [-e <value>...] [--json-schema] [--listing <value>] [--no-color]
9718-
[--password <value>] [--path <value>] [-s <value>] [--verbose]
9717+
$ shopify theme share [--auth-alias <value>] [-e <value>...] [-j] [--json-schema] [--listing <value>]
9718+
[--no-color] [--password <value>] [--path <value>] [-s <value>] [--verbose]
97199719

97209720
FLAGS
97219721
-e, --environment=<value>...
97229722
The environment to apply to the current command.
97239723
[env: SHOPIFY_FLAG_ENVIRONMENT]
97249724

9725+
-j, --json
9726+
Output the result as JSON. Automatically disables color output.
9727+
[env: SHOPIFY_FLAG_JSON]
9728+
97259729
-s, --store=<value>
97269730
Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com,
97279731
https://example.myshopify.com).
@@ -9763,6 +9767,111 @@ DESCRIPTION
97639767
This command returns a "preview link"
97649768
(https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can
97659769
share with others.
9770+
9771+
Use `--json-schema` to print the result, error, and event schemas.
9772+
9773+
Output from `--json` conforms to the `ThemeShareResult` schema.
9774+
9775+
```json
9776+
{
9777+
"anyOf": [
9778+
{
9779+
"type": "object",
9780+
"properties": {
9781+
"status": {
9782+
"type": "string",
9783+
"enum": [
9784+
"success",
9785+
"failed"
9786+
]
9787+
},
9788+
"environment": {
9789+
"type": "string"
9790+
},
9791+
"theme": {
9792+
"$ref": "#/definitions/ThemePushTheme"
9793+
}
9794+
},
9795+
"required": [
9796+
"status",
9797+
"theme"
9798+
],
9799+
"additionalProperties": false
9800+
},
9801+
{
9802+
"type": "array",
9803+
"items": {
9804+
"type": "object",
9805+
"properties": {
9806+
"status": {
9807+
"$ref": "#/definitions/ThemeShareResult/anyOf/0/properties/status"
9808+
},
9809+
"environment": {
9810+
"type": "string"
9811+
},
9812+
"theme": {
9813+
"$ref": "#/definitions/ThemePushTheme"
9814+
}
9815+
},
9816+
"required": [
9817+
"status",
9818+
"environment",
9819+
"theme"
9820+
],
9821+
"additionalProperties": false
9822+
}
9823+
}
9824+
],
9825+
"title": "ThemeShareResult",
9826+
"definitions": {
9827+
"ThemePushTheme": {
9828+
"type": "object",
9829+
"properties": {
9830+
"id": {
9831+
"type": "number"
9832+
},
9833+
"name": {
9834+
"type": "string"
9835+
},
9836+
"role": {
9837+
"type": "string"
9838+
},
9839+
"shop": {
9840+
"type": "string"
9841+
},
9842+
"editor_url": {
9843+
"type": "string"
9844+
},
9845+
"preview_url": {
9846+
"type": "string"
9847+
},
9848+
"warning": {
9849+
"type": "string"
9850+
},
9851+
"errors": {
9852+
"type": "object",
9853+
"additionalProperties": {
9854+
"type": "array",
9855+
"items": {
9856+
"type": "string"
9857+
}
9858+
}
9859+
}
9860+
},
9861+
"required": [
9862+
"id",
9863+
"name",
9864+
"role",
9865+
"shop",
9866+
"editor_url",
9867+
"preview_url"
9868+
],
9869+
"additionalProperties": false
9870+
}
9871+
},
9872+
"$schema": "http://json-schema.org/draft-07/schema#"
9873+
}
9874+
```
97669875
```
97679876
97689877
## `shopify upgrade`

‎packages/cli/oclif.manifest.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11696,7 +11696,7 @@
1169611696
"args": {
1169711697
},
1169811698
"customPluginName": "@shopify/theme",
11699-
"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.",
11699+
"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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeShareResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"enum\": [\n \"success\",\n \"failed\"\n ]\n },\n \"environment\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePushTheme\"\n }\n },\n \"required\": [\n \"status\",\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"$ref\": \"#/definitions/ThemeShareResult/anyOf/0/properties/status\"\n },\n \"environment\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePushTheme\"\n }\n },\n \"required\": [\n \"status\",\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```",
1170011700
"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.",
1170111701
"enableJsonFlag": false,
1170211702
"flags": {
@@ -11726,6 +11726,15 @@
1172611726
"name": "force",
1172711727
"type": "boolean"
1172811728
},
11729+
"json": {
11730+
"allowNo": false,
11731+
"char": "j",
11732+
"description": "Output the result as JSON. Automatically disables color output.",
11733+
"env": "SHOPIFY_FLAG_JSON",
11734+
"hidden": false,
11735+
"name": "json",
11736+
"type": "boolean"
11737+
},
1172911738
"json-schema": {
1173011739
"allowNo": false,
1173111740
"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
@@ -49,7 +49,6 @@ const commandExceptions = [
4949
'packages/theme/src/cli/commands/theme/package.ts',
5050
'packages/theme/src/cli/commands/theme/preview.ts',
5151
'packages/theme/src/cli/commands/theme/profile.ts',
52-
'packages/theme/src/cli/commands/theme/share.ts',
5352

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

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

Lines changed: 20 additions & 3 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,6 +15,10 @@ 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.
@@ -23,6 +29,7 @@ export default class Share extends ThemeCommand {
2329

2430
static flags = {
2531
...globalFlags,
32+
...jsonFlag,
2633
...themeFlags,
2734
force: Flags.boolean({
2835
hidden: true,
@@ -59,7 +66,17 @@ export default class Share extends ThemeCommand {
5966
}
6067

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

0 commit comments

Comments
 (0)