Skip to content

Commit 289efda

Browse files
Add typed JSON output to theme open
1 parent e18fe9c commit 289efda

13 files changed

Lines changed: 375 additions & 84 deletions

File tree

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

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 and a discoverable result schema to theme open.

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7613,6 +7613,15 @@
76137613
"isOptional": true,
76147614
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
76157615
},
7616+
{
7617+
"filePath": "docs-shopify.dev/commands/interfaces/theme-open.interface.ts",
7618+
"syntaxKind": "PropertySignature",
7619+
"name": "-j, --json",
7620+
"value": "''",
7621+
"description": "Output the result as JSON. Automatically disables color output.",
7622+
"isOptional": true,
7623+
"environmentValue": "SHOPIFY_FLAG_JSON"
7624+
},
76167625
{
76177626
"filePath": "docs-shopify.dev/commands/interfaces/theme-open.interface.ts",
76187627
"syntaxKind": "PropertySignature",
@@ -7641,7 +7650,7 @@
76417650
"environmentValue": "SHOPIFY_FLAG_THEME_ID"
76427651
}
76437652
],
7644-
"value": "export interface themeopen {\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 * Open your development theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\n\n /**\n * Open the theme editor for the specified theme in the browser.\n * @environment SHOPIFY_FLAG_EDITOR\n */\n '-E, --editor'?: ''\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 * Open your live (published) theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\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}"
7653+
"value": "export interface themeopen {\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 * Open your development theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\n\n /**\n * Open the theme editor for the specified theme in the browser.\n * @environment SHOPIFY_FLAG_EDITOR\n */\n '-E, --editor'?: ''\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 * Open your live (published) theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\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}"
76457654
}
76467655
},
76477656
"themepackage": {

‎packages/cli/README.md‎

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7793,7 +7793,7 @@ Opens the preview of your remote theme.
77937793
77947794
```
77957795
USAGE
7796-
$ shopify theme open [--auth-alias <value>] [-d] [-E] [-e <value>...] [--json-schema] [-l] [--no-color]
7796+
$ shopify theme open [--auth-alias <value>] [-d] [-E] [-e <value>...] [-j] [--json-schema] [-l] [--no-color]
77977797
[--password <value>] [--path <value>] [-s <value>] [-t <value>] [--verbose]
77987798

77997799
FLAGS
@@ -7809,6 +7809,10 @@ FLAGS
78097809
The environment to apply to the current command.
78107810
[env: SHOPIFY_FLAG_ENVIRONMENT]
78117811

7812+
-j, --json
7813+
Output the result as JSON. Automatically disables color output.
7814+
[env: SHOPIFY_FLAG_JSON]
7815+
78127816
-l, --live
78137817
Open your live (published) theme. Use --development, --live, or --theme in non-interactive environments.
78147818
[env: SHOPIFY_FLAG_LIVE]
@@ -7858,6 +7862,68 @@ DESCRIPTION
78587862

78597863
If you don't specify a theme, then you're prompted to select the theme to open from the list of the themes in your
78607864
store.
7865+
7866+
Output from `--json` conforms to the `ThemeOpenResult` schema.
7867+
7868+
Use `--json-schema` to print the result, error, and event schemas.
7869+
7870+
```json
7871+
{
7872+
"type": "object",
7873+
"properties": {
7874+
"theme": {
7875+
"$ref": "#/definitions/Theme"
7876+
},
7877+
"preview_url": {
7878+
"type": "string"
7879+
},
7880+
"editor_url": {
7881+
"type": "string"
7882+
}
7883+
},
7884+
"required": [
7885+
"theme",
7886+
"preview_url",
7887+
"editor_url"
7888+
],
7889+
"additionalProperties": false,
7890+
"title": "ThemeOpenResult",
7891+
"definitions": {
7892+
"Theme": {
7893+
"type": "object",
7894+
"properties": {
7895+
"id": {
7896+
"type": "number"
7897+
},
7898+
"name": {
7899+
"type": "string"
7900+
},
7901+
"createdAtRuntime": {
7902+
"type": "boolean"
7903+
},
7904+
"processing": {
7905+
"type": "boolean"
7906+
},
7907+
"role": {
7908+
"type": "string"
7909+
},
7910+
"src": {
7911+
"type": "string"
7912+
}
7913+
},
7914+
"required": [
7915+
"id",
7916+
"name",
7917+
"createdAtRuntime",
7918+
"processing",
7919+
"role"
7920+
],
7921+
"additionalProperties": false
7922+
}
7923+
},
7924+
"$schema": "http://json-schema.org/draft-07/schema#"
7925+
}
7926+
```
78617927
```
78627928
78637929
## `shopify theme package`

‎packages/cli/oclif.manifest.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10593,7 +10593,7 @@
1059310593
"args": {
1059410594
},
1059510595
"customPluginName": "@shopify/theme",
10596-
"description": "Returns links that let you preview the specified theme. The following links are returned:\n\n - A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n If you don't specify a theme, then you're prompted to select the theme to open from the list of the themes in your store.",
10596+
"description": "Returns links that let you preview the specified theme. The following links are returned:\n\n - A link to the \"editor\" (https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n If you don't specify a theme, then you're prompted to select the theme to open from the list of the themes in your store.\n\nOutput from `--json` conforms to the `ThemeOpenResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"theme\": {\n \"$ref\": \"#/definitions/Theme\"\n },\n \"preview_url\": {\n \"type\": \"string\"\n },\n \"editor_url\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"theme\",\n \"preview_url\",\n \"editor_url\"\n ],\n \"additionalProperties\": false,\n \"title\": \"ThemeOpenResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"createdAtRuntime\": {\n \"type\": \"boolean\"\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"role\": {\n \"type\": \"string\"\n },\n \"src\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"createdAtRuntime\",\n \"processing\",\n \"role\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
1059710597
"descriptionWithMarkdown": "Returns links that let you preview the specified theme. The following links are returned:\n\n - A link to the [editor](https://shopify.dev/docs/themes/tools/online-editor) for the theme in the Shopify admin.\n - A [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with other developers.\n\n If you don't specify a theme, then you're prompted to select the theme to open from the list of the themes in your store.",
1059810598
"enableJsonFlag": false,
1059910599
"flags": {
@@ -10630,6 +10630,15 @@
1063010630
"name": "environment",
1063110631
"type": "option"
1063210632
},
10633+
"json": {
10634+
"allowNo": false,
10635+
"char": "j",
10636+
"description": "Output the result as JSON. Automatically disables color output.",
10637+
"env": "SHOPIFY_FLAG_JSON",
10638+
"hidden": false,
10639+
"name": "json",
10640+
"type": "boolean"
10641+
},
1063310642
"json-schema": {
1063410643
"allowNo": false,
1063510644
"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
@@ -67,7 +67,6 @@ const commandExceptions = [
6767
'packages/theme/src/cli/commands/theme/init.ts',
6868
'packages/theme/src/cli/commands/theme/list.ts',
6969
'packages/theme/src/cli/commands/theme/metafields/pull.ts',
70-
'packages/theme/src/cli/commands/theme/open.ts',
7170
'packages/theme/src/cli/commands/theme/package.ts',
7271
'packages/theme/src/cli/commands/theme/preview.ts',
7372
'packages/theme/src/cli/commands/theme/profile.ts',
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import Open from './open.js'
2+
import {open} from '../../services/open.js'
3+
import {themeOpenJsonOutputSchema, type ThemeOpenResult} from '../../services/open/types.js'
4+
import {ensureThemeStore} from '../../utilities/theme-store.js'
5+
import {captureStandardStreams} from '../../utilities/testing/output.js'
6+
import {Config} from '@oclif/core'
7+
import {expect, test, vi} from 'vitest'
8+
import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
9+
import {openURL} from '@shopify/cli-kit/node/system'
10+
import {renderInfo} from '@shopify/cli-kit/node/ui'
11+
12+
vi.mock('../../services/open.js')
13+
vi.mock('../../utilities/theme-store.js')
14+
vi.mock('@shopify/cli-kit/node/session')
15+
vi.mock('@shopify/cli-kit/node/system', async (importOriginal) => ({
16+
...(await importOriginal<typeof import('@shopify/cli-kit/node/system')>()),
17+
openURL: vi.fn(),
18+
}))
19+
vi.mock('@shopify/cli-kit/node/ui')
20+
vi.mock('@shopify/cli-kit/node/environments')
21+
vi.mock('@shopify/cli-kit/node/analytics')
22+
vi.mock('@shopify/cli-kit/node/metadata')
23+
24+
const session = {token: 'token', storeFqdn: 'store.myshopify.com'}
25+
const result: ThemeOpenResult = {
26+
theme: {id: 1, name: 'my theme', role: 'live', processing: false, createdAtRuntime: false},
27+
preview_url: 'https://store.myshopify.com?preview_theme_id=1',
28+
editor_url: 'https://store.myshopify.com/admin/themes/1/editor',
29+
}
30+
31+
async function run(argv: string[]) {
32+
const config = new Config({root: __dirname})
33+
await config.load()
34+
vi.mocked(ensureThemeStore).mockReturnValue(session.storeFqdn)
35+
vi.mocked(ensureAuthenticatedThemes).mockResolvedValue(session)
36+
await new Open(['--store=store.myshopify.com', ...argv], config).run()
37+
}
38+
39+
test('exposes the result schema and JSON flag in help', () => {
40+
expect(Open.jsonOutputSchema).toBe(themeOpenJsonOutputSchema)
41+
expect(Open.flags.json).toBeDefined()
42+
expect(Open.description).toContain('ThemeOpenResult')
43+
expect(Open.description).toContain('preview_url')
44+
})
45+
46+
test.each([false, true])('preserves browser selection with editor=%s', async (editor) => {
47+
vi.mocked(open).mockResolvedValue(result)
48+
49+
await run(['--theme=1', ...(editor ? ['--editor'] : [])])
50+
51+
expect(renderInfo).toHaveBeenCalled()
52+
expect(openURL).toHaveBeenCalledWith(editor ? result.editor_url : result.preview_url)
53+
})
54+
55+
test('writes one JSON result', async () => {
56+
vi.mocked(open).mockResolvedValue(result)
57+
58+
const streams = await captureStandardStreams(() => run(['--theme=1', '--json']))
59+
60+
expect(JSON.parse(streams.stdout)).toEqual(result)
61+
expect(streams.stderr).toBe('')
62+
expect(renderInfo).not.toHaveBeenCalled()
63+
expect(openURL).toHaveBeenCalledWith(result.preview_url)
64+
expect(open).toHaveBeenCalledWith(session, expect.objectContaining({theme: '1', json: true}))
65+
})
66+
67+
test('propagates selection errors without opening the browser or rendering a result', async () => {
68+
const error = new Error('Theme not found')
69+
vi.mocked(open).mockRejectedValue(error)
70+
71+
await expect(run(['--theme=1', '--json'])).rejects.toBe(error)
72+
73+
expect(openURL).not.toHaveBeenCalled()
74+
expect(renderInfo).not.toHaveBeenCalled()
75+
})
76+
77+
test('preserves browser failures after writing the result', async () => {
78+
vi.mocked(open).mockResolvedValue(result)
79+
const error = new Error('Browser unavailable')
80+
vi.mocked(openURL).mockRejectedValue(error)
81+
82+
const streams = await captureStandardStreams(async () => {
83+
await expect(run(['--theme=1', '--json'])).rejects.toBe(error)
84+
})
85+
86+
expect(JSON.parse(streams.stdout)).toEqual(result)
87+
})

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

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,21 @@
11
import {open} from '../../services/open.js'
2+
import {renderThemeOpenResult} from '../../services/open/result.js'
3+
import {themeOpenJsonOutputSchema} from '../../services/open/types.js'
24
import {themeFlags} from '../../flags.js'
35
import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.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'
8+
import {openURL} from '@shopify/cli-kit/node/system'
69
import {AdminSession} from '@shopify/cli-kit/node/session'
710
import {InferredFlags} from '@oclif/core/interfaces'
811
import type {NonTTYFlagRequirement} from '@shopify/cli-kit/node/base-command'
912

1013
type OpenFlags = InferredFlags<typeof Open.flags>
1114
export default class Open extends ThemeCommand {
15+
static get jsonOutputSchema() {
16+
return themeOpenJsonOutputSchema
17+
}
18+
1219
static summary = 'Opens the preview of your remote theme.'
1320

1421
static descriptionWithMarkdown = `Returns links that let you preview the specified theme. The following links are returned:
@@ -22,6 +29,7 @@ export default class Open extends ThemeCommand {
2229

2330
static flags = {
2431
...globalFlags,
32+
...jsonFlag,
2533
...themeFlags,
2634
development: Flags.boolean({
2735
char: 'd',
@@ -55,6 +63,8 @@ export default class Open extends ThemeCommand {
5563
}
5664

5765
async command(flags: OpenFlags, adminSession: AdminSession) {
58-
await open(adminSession, flags)
66+
const result = await open(adminSession, flags)
67+
renderThemeOpenResult(result, flags.json ? 'json' : 'text')
68+
await openURL(flags.editor ? result.editor_url : result.preview_url)
5969
}
6070
}

‎packages/theme/src/cli/services/open.test.ts‎

Lines changed: 14 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -40,63 +40,25 @@ const options = {
4040
}
4141

4242
describe('open', () => {
43-
test('opens the preview URL', async () => {
44-
// Given
43+
test('returns theme details and URLs without opening a browser or rendering', async () => {
4544
vi.mocked(findOrSelectTheme).mockResolvedValue(theme)
4645

47-
// When
48-
await open(session, options)
49-
50-
// Then
51-
expect(openURL).toBeCalledWith('https://my-shop.myshopify.com?preview_theme_id=1')
52-
})
53-
54-
test('opens the editor URL with `editor` flag', async () => {
55-
// Given
56-
vi.mocked(findOrSelectTheme).mockResolvedValue(theme)
57-
58-
// When
59-
await open(session, {...options, editor: true})
60-
61-
// Then
62-
expect(openURL).toBeCalledWith('https://my-shop.myshopify.com/admin/themes/1/editor')
46+
await expect(open(session, options)).resolves.toEqual({
47+
theme,
48+
preview_url: 'https://my-shop.myshopify.com?preview_theme_id=1',
49+
editor_url: 'https://my-shop.myshopify.com/admin/themes/1/editor',
50+
})
51+
expect(openURL).not.toHaveBeenCalled()
52+
expect(renderInfo).not.toHaveBeenCalled()
6353
})
6454

65-
test('renders the theme links', async () => {
66-
// Given
67-
vi.spyOn(DevelopmentThemeManager.prototype, 'fetch').mockResolvedValue(theme)
68-
vi.mocked(findOrSelectTheme).mockResolvedValue(theme)
55+
test('propagates theme selection failures', async () => {
56+
const error = new Error('Theme not found')
57+
vi.mocked(findOrSelectTheme).mockRejectedValue(error)
6958

70-
// When
71-
await open(session, options)
72-
73-
// Then
74-
expect(renderInfo).toBeCalledWith({
75-
body: [
76-
'Preview information for theme',
77-
"'my theme'",
78-
{subdued: '(#1)'},
79-
'\n\n',
80-
{
81-
list: {
82-
items: [
83-
{
84-
link: {
85-
label: 'Preview your theme',
86-
url: 'https://my-shop.myshopify.com?preview_theme_id=1',
87-
},
88-
},
89-
{
90-
link: {
91-
label: 'Customize your theme at the theme editor',
92-
url: 'https://my-shop.myshopify.com/admin/themes/1/editor',
93-
},
94-
},
95-
],
96-
},
97-
},
98-
],
99-
})
59+
await expect(open(session, options)).rejects.toBe(error)
60+
expect(openURL).not.toHaveBeenCalled()
61+
expect(renderInfo).not.toHaveBeenCalled()
10062
})
10163

10264
describe('findOrSelectTheme', () => {

0 commit comments

Comments
 (0)