Skip to content

Commit ebf024f

Browse files
Add typed JSON output to theme preview
1 parent 289efda commit ebf024f

14 files changed

Lines changed: 309 additions & 159 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': patch
3+
---
4+
5+
Expose the theme preview JSON result schema and report browser failures as typed diagnostics.

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7725,15 +7725,6 @@
77257725
"isOptional": true,
77267726
"environmentValue": "SHOPIFY_FLAG_AUTH_ALIAS"
77277727
},
7728-
{
7729-
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
7730-
"syntaxKind": "PropertySignature",
7731-
"name": "--json",
7732-
"value": "''",
7733-
"description": "Output the preview URL and identifier as JSON.",
7734-
"isOptional": true,
7735-
"environmentValue": "SHOPIFY_FLAG_JSON"
7736-
},
77377728
{
77387729
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
77397730
"syntaxKind": "PropertySignature",
@@ -7814,6 +7805,15 @@
78147805
"isOptional": true,
78157806
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
78167807
},
7808+
{
7809+
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
7810+
"syntaxKind": "PropertySignature",
7811+
"name": "-j, --json",
7812+
"value": "''",
7813+
"description": "Output the preview URL and identifier as JSON.",
7814+
"isOptional": true,
7815+
"environmentValue": "SHOPIFY_FLAG_JSON"
7816+
},
78177817
{
78187818
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
78197819
"syntaxKind": "PropertySignature",
@@ -7832,7 +7832,7 @@
78327832
"environmentValue": "SHOPIFY_FLAG_THEME_ID"
78337833
}
78347834
],
7835-
"value": "export interface themepreview {\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 preview URL and identifier as JSON.\n * @environment SHOPIFY_FLAG_JSON\n */\n '--json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Automatically launch the theme preview in your default web browser.\n * @environment SHOPIFY_FLAG_OPEN\n */\n '--open'?: ''\n\n /**\n * Path to a JSON overrides file.\n * @environment SHOPIFY_FLAG_OVERRIDES\n */\n '--overrides <value>': string\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 * An existing preview identifier to update instead of creating a new preview.\n * @environment SHOPIFY_FLAG_PREVIEW_ID\n */\n '--preview-id <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.\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}"
7835+
"value": "export interface themepreview {\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 preview URL and identifier as JSON.\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 * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Automatically launch the theme preview in your default web browser.\n * @environment SHOPIFY_FLAG_OPEN\n */\n '--open'?: ''\n\n /**\n * Path to a JSON overrides file.\n * @environment SHOPIFY_FLAG_OVERRIDES\n */\n '--overrides <value>': string\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 * An existing preview identifier to update instead of creating a new preview.\n * @environment SHOPIFY_FLAG_PREVIEW_ID\n */\n '--preview-id <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.\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}"
78367836
}
78377837
},
78387838
"themeprofile": {

‎packages/cli/README.md‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7977,7 +7977,7 @@ Applies JSON overrides to a theme and returns a preview URL.
79777977
79787978
```
79797979
USAGE
7980-
$ shopify theme preview --overrides <value> -t <value> [--auth-alias <value>] [-e <value>...] [--json]
7980+
$ shopify theme preview --overrides <value> -t <value> [--auth-alias <value>] [-e <value>...] [-j]
79817981
[--json-schema] [--no-color] [--open] [--password <value>] [--path <value>] [--preview-id <value>] [-s <value>]
79827982
[--verbose]
79837983

@@ -7986,6 +7986,10 @@ FLAGS
79867986
The environment to apply to the current command.
79877987
[env: SHOPIFY_FLAG_ENVIRONMENT]
79887988

7989+
-j, --json
7990+
Output the preview URL and identifier as JSON.
7991+
[env: SHOPIFY_FLAG_JSON]
7992+
79897993
-s, --store=<value>
79907994
Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com,
79917995
https://example.myshopify.com).
@@ -7999,10 +8003,6 @@ FLAGS
79998003
Alias of the Shopify account to use for authentication.
80008004
[env: SHOPIFY_FLAG_AUTH_ALIAS]
80018005

8002-
--json
8003-
Output the preview URL and identifier as JSON.
8004-
[env: SHOPIFY_FLAG_JSON]
8005-
80068006
--json-schema
80078007
Print the command's JSON schemas.
80088008
[env: SHOPIFY_FLAG_JSON_SCHEMA]
@@ -8042,6 +8042,31 @@ DESCRIPTION
80428042

80438043
The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id`
80448044
to update an existing preview instead of creating a new one.
8045+
8046+
Output from `--json` conforms to the `ThemePreviewResult` schema.
8047+
8048+
Use `--json-schema` to print the result, error, and event schemas.
8049+
8050+
```json
8051+
{
8052+
"type": "object",
8053+
"properties": {
8054+
"url": {
8055+
"type": "string"
8056+
},
8057+
"preview_identifier": {
8058+
"type": "string"
8059+
}
8060+
},
8061+
"required": [
8062+
"url",
8063+
"preview_identifier"
8064+
],
8065+
"additionalProperties": false,
8066+
"title": "ThemePreviewResult",
8067+
"$schema": "http://json-schema.org/draft-07/schema#"
8068+
}
8069+
```
80458070
```
80468071
80478072
## `shopify theme profile`

‎packages/cli/oclif.manifest.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10785,7 +10785,7 @@
1078510785
"args": {
1078610786
},
1078710787
"customPluginName": "@shopify/theme",
10788-
"description": "Applies a JSON overrides file to a theme and creates or updates a preview. This lets you quickly preview changes.\n\n The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id` to update an existing preview instead of creating a new one.",
10788+
"description": "Applies a JSON overrides file to a theme and creates or updates a preview. This lets you quickly preview changes.\n\n The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id` to update an existing preview instead of creating a new one.\n\nOutput from `--json` conforms to the `ThemePreviewResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"type\": \"object\",\n \"properties\": {\n \"url\": {\n \"type\": \"string\"\n },\n \"preview_identifier\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"url\",\n \"preview_identifier\"\n ],\n \"additionalProperties\": false,\n \"title\": \"ThemePreviewResult\",\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
1078910789
"descriptionWithMarkdown": "Applies a JSON overrides file to a theme and creates or updates a preview. This lets you quickly preview changes.\n\n The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id` to update an existing preview instead of creating a new one.",
1079010790
"enableJsonFlag": false,
1079110791
"flags": {
@@ -10808,8 +10808,10 @@
1080810808
},
1080910809
"json": {
1081010810
"allowNo": false,
10811+
"char": "j",
1081110812
"description": "Output the preview URL and identifier as JSON.",
1081210813
"env": "SHOPIFY_FLAG_JSON",
10814+
"hidden": false,
1081310815
"name": "json",
1081410816
"type": "boolean"
1081510817
},

‎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/list.ts',
6969
'packages/theme/src/cli/commands/theme/metafields/pull.ts',
7070
'packages/theme/src/cli/commands/theme/package.ts',
71-
'packages/theme/src/cli/commands/theme/preview.ts',
7271
'packages/theme/src/cli/commands/theme/profile.ts',
7372
'packages/theme/src/cli/commands/theme/publish.ts',
7473
'packages/theme/src/cli/commands/theme/pull.ts',

‎packages/theme/src/cli/commands/theme/preview.test.ts‎

Lines changed: 77 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,23 @@
11
import Preview from './preview.js'
2+
import {themePreviewJsonOutputSchema} from '../../services/dev-override/types.js'
3+
import {captureStandardStreams} from '../../utilities/testing/output.js'
24
import {devWithOverrideFile} from '../../services/dev-override.js'
35
import {findOrSelectTheme} from '../../utilities/theme-selector.js'
46
import {ensureThemeStore} from '../../utilities/theme-store.js'
7+
import {openURL} from '@shopify/cli-kit/node/system'
8+
import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events'
9+
import {renderSuccess} from '@shopify/cli-kit/node/ui'
510
import {buildTheme} from '@shopify/cli-kit/node/themes/factories'
611
import {recordEvent} from '@shopify/cli-kit/node/analytics'
712
import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
813
import {Config} from '@oclif/core'
914
import {describe, vi, expect, test, beforeEach} from 'vitest'
1015

16+
vi.mock('@shopify/cli-kit/node/ui')
17+
vi.mock('@shopify/cli-kit/node/system', async (importOriginal) => ({
18+
...(await importOriginal<typeof import('@shopify/cli-kit/node/system')>()),
19+
openURL: vi.fn(),
20+
}))
1121
vi.mock('@shopify/cli-kit/node/session')
1222
vi.mock('@shopify/cli-kit/node/analytics', () => ({
1323
recordEvent: vi.fn(),
@@ -24,6 +34,8 @@ vi.mock('../../utilities/theme-store.js')
2434

2535
const CommandConfig = new Config({root: __dirname})
2636

37+
const result = {url: 'https://abc123.shopifypreview.com', preview_identifier: 'abc123'}
38+
2739
const adminSession = {token: 'test-token', storeFqdn: 'test-store.myshopify.com'}
2840
const namedTheme = buildTheme({id: 2, name: 'My Theme', role: 'unpublished'})!
2941

@@ -38,7 +50,7 @@ describe('Preview', () => {
3850
vi.mocked(ensureThemeStore).mockReturnValue('test-store.myshopify.com')
3951
vi.mocked(ensureAuthenticatedThemes).mockResolvedValue(adminSession)
4052
vi.mocked(findOrSelectTheme).mockResolvedValue(namedTheme)
41-
vi.mocked(devWithOverrideFile).mockResolvedValue(undefined)
53+
vi.mocked(devWithOverrideFile).mockResolvedValue(result)
4254
})
4355

4456
test('calls devWithOverrideFile with minimum options passed into the command', async () => {
@@ -52,7 +64,6 @@ describe('Preview', () => {
5264
adminSession,
5365
overrideJson: '/path/to/overrides.json',
5466
themeId: expectedTheme.id.toString(),
55-
open: false,
5667
}),
5768
)
5869
})
@@ -71,16 +82,18 @@ describe('Preview', () => {
7182
)
7283
})
7384

74-
test('passes --open to devWithOverrideFile when provided', async () => {
85+
test('opens the resulting preview when requested', async () => {
86+
vi.mocked(openURL).mockResolvedValue(true)
7587
const expectedTheme = buildTheme({id: 5, name: 'Expected Theme', role: 'unpublished'})!
7688
vi.mocked(findOrSelectTheme).mockResolvedValue(expectedTheme)
7789

7890
await run(['--overrides=/path/to/overrides.json', `--theme=${expectedTheme.id}`, '--open'])
7991

92+
expect(openURL).toHaveBeenCalledWith(result.url)
93+
8094
expect(devWithOverrideFile).toHaveBeenCalledWith(
8195
expect.objectContaining({
8296
themeId: expectedTheme.id.toString(),
83-
open: true,
8497
}),
8598
)
8699
})
@@ -108,17 +121,68 @@ describe('Preview', () => {
108121
)
109122
})
110123

111-
test('passes --json to devWithOverrideFile when provided', async () => {
112-
const expectedTheme = buildTheme({id: 5, name: 'Expected Theme', role: 'unpublished'})!
113-
vi.mocked(findOrSelectTheme).mockResolvedValue(expectedTheme)
124+
test('preserves the existing JSON output channel through the real presenter and writer', async () => {
125+
const streams = await captureStandardStreams(() =>
126+
runWithCommandEventsForCommand(['--json'], () =>
127+
run(['--overrides=/path/to/overrides.json', '--theme=2', '--json']),
128+
),
129+
)
114130

115-
await run(['--overrides=/path/to/overrides.json', `--theme=${expectedTheme.id}`, '--json'])
131+
expect(streams.stdout).toBe('')
132+
expect(JSON.parse(streams.stderr)).toMatchObject({
133+
type: 'diagnostic',
134+
level: 'info',
135+
message: JSON.stringify(result),
136+
})
137+
expect(renderSuccess).not.toHaveBeenCalled()
138+
expect(openURL).not.toHaveBeenCalled()
139+
expect(devWithOverrideFile).toHaveBeenCalledWith({
140+
adminSession,
141+
overrideJson: '/path/to/overrides.json',
142+
themeId: '2',
143+
previewIdentifier: undefined,
144+
password: undefined,
145+
})
146+
})
116147

117-
expect(devWithOverrideFile).toHaveBeenCalledWith(
118-
expect.objectContaining({
119-
themeId: expectedTheme.id.toString(),
120-
json: true,
121-
}),
148+
test('exposes its result schema in help', () => {
149+
expect(Preview.jsonOutputSchema).toBe(themePreviewJsonOutputSchema)
150+
expect(Preview.description).toContain('ThemePreviewResult')
151+
expect(Preview.description).toContain('preview_identifier')
152+
expect(Preview.flags.json.env).toBe('SHOPIFY_FLAG_JSON')
153+
})
154+
155+
test('propagates failures without printing a success result or opening a browser', async () => {
156+
const error = new Error('Failed to parse override file')
157+
vi.mocked(devWithOverrideFile).mockRejectedValue(error)
158+
159+
const streams = await captureStandardStreams(async () => {
160+
await expect(run(['--overrides=/path/to/overrides.json', '--theme=2', '--json', '--open'])).rejects.toBe(error)
161+
})
162+
163+
expect(streams).toEqual({stdout: '', stderr: ''})
164+
expect(renderSuccess).not.toHaveBeenCalled()
165+
expect(openURL).not.toHaveBeenCalled()
166+
})
167+
168+
test('keeps browser failures nonfatal and sends a typed warning to stderr', async () => {
169+
const error = new Error('Browser unavailable')
170+
vi.mocked(openURL).mockRejectedValue(error)
171+
172+
const streams = await captureStandardStreams(() =>
173+
runWithCommandEventsForCommand(['--json'], () =>
174+
run(['--overrides=/path/to/overrides.json', '--theme=2', '--json', '--open']),
175+
),
122176
)
177+
178+
const events = streams.stderr
179+
.trim()
180+
.split('\n')
181+
.map((line) => JSON.parse(line))
182+
expect(events).toMatchObject([
183+
{type: 'diagnostic', level: 'info', message: JSON.stringify(result)},
184+
{type: 'diagnostic', level: 'warning', message: `Failed to open theme preview.\n${error.stack}`},
185+
])
186+
expect(streams.stdout).toBe('')
123187
})
124188
})

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

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
import {themeFlags} from '../../flags.js'
22
import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.js'
33
import {devWithOverrideFile} from '../../services/dev-override.js'
4+
import {renderThemePreviewResult, renderThemePreviewOpenError} from '../../services/dev-override/result.js'
5+
import {themePreviewJsonOutputSchema} from '../../services/dev-override/types.js'
46
import {findOrSelectTheme} from '../../utilities/theme-selector.js'
57
import {Flags} from '@oclif/core'
6-
import {globalFlags} from '@shopify/cli-kit/node/cli'
8+
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
9+
import {openURL} from '@shopify/cli-kit/node/system'
710
import {AdminSession} from '@shopify/cli-kit/node/session'
811
import {InferredFlags} from '@oclif/core/interfaces'
912

1013
type PreviewFlags = InferredFlags<typeof Preview.flags>
1114

1215
export default class Preview extends ThemeCommand {
16+
static get jsonOutputSchema() {
17+
return themePreviewJsonOutputSchema
18+
}
19+
1320
static summary = 'Applies JSON overrides to a theme and returns a preview URL.'
1421

1522
static descriptionWithMarkdown = `Applies a JSON overrides file to a theme and creates or updates a preview. This lets you quickly preview changes.
@@ -20,6 +27,7 @@ export default class Preview extends ThemeCommand {
2027

2128
static flags = {
2229
...globalFlags,
30+
...jsonFlag,
2331
...themeFlags,
2432
theme: Flags.string({
2533
char: 't',
@@ -42,6 +50,7 @@ export default class Preview extends ThemeCommand {
4250
default: false,
4351
}),
4452
json: Flags.boolean({
53+
...jsonFlag.json,
4554
description: 'Output the preview URL and identifier as JSON.',
4655
env: 'SHOPIFY_FLAG_JSON',
4756
default: false,
@@ -52,14 +61,17 @@ export default class Preview extends ThemeCommand {
5261

5362
async command(flags: PreviewFlags, adminSession: AdminSession) {
5463
const theme = await findOrSelectTheme(adminSession, {filter: {theme: flags.theme}})
55-
await devWithOverrideFile({
64+
const result = await devWithOverrideFile({
5665
adminSession,
5766
overrideJson: flags.overrides,
5867
themeId: theme.id.toString(),
5968
previewIdentifier: flags['preview-id'],
60-
open: flags.open,
6169
password: flags.password,
62-
json: flags.json,
6370
})
71+
const format = flags.json ? 'json' : 'text'
72+
renderThemePreviewResult(result, format, Boolean(flags['preview-id']))
73+
if (flags.open) {
74+
openURL(result.url).catch((error: Error) => renderThemePreviewOpenError(error, format))
75+
}
6476
}
6577
}

0 commit comments

Comments
 (0)