Skip to content

Commit 814fcb9

Browse files
Add typed JSON output to theme preview
1 parent 36bed07 commit 814fcb9

13 files changed

Lines changed: 309 additions & 160 deletions

File tree

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

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7734,15 +7734,6 @@
77347734
"isOptional": true,
77357735
"environmentValue": "SHOPIFY_FLAG_AUTH_ALIAS"
77367736
},
7737-
{
7738-
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
7739-
"syntaxKind": "PropertySignature",
7740-
"name": "--json",
7741-
"value": "''",
7742-
"description": "Output the preview URL and identifier as JSON.",
7743-
"isOptional": true,
7744-
"environmentValue": "SHOPIFY_FLAG_JSON"
7745-
},
77467737
{
77477738
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
77487739
"syntaxKind": "PropertySignature",
@@ -7823,6 +7814,15 @@
78237814
"isOptional": true,
78247815
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
78257816
},
7817+
{
7818+
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
7819+
"syntaxKind": "PropertySignature",
7820+
"name": "-j, --json",
7821+
"value": "''",
7822+
"description": "Output the preview URL and identifier as JSON.",
7823+
"isOptional": true,
7824+
"environmentValue": "SHOPIFY_FLAG_JSON"
7825+
},
78267826
{
78277827
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
78287828
"syntaxKind": "PropertySignature",
@@ -7841,7 +7841,7 @@
78417841
"environmentValue": "SHOPIFY_FLAG_THEME_ID"
78427842
}
78437843
],
7844-
"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}"
7844+
"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}"
78457845
}
78467846
},
78477847
"themeprofile": {

‎packages/cli/README.md‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8333,7 +8333,7 @@ Applies JSON overrides to a theme and returns a preview URL.
83338333
83348334
```
83358335
USAGE
8336-
$ shopify theme preview --overrides <value> -t <value> [--auth-alias <value>] [-e <value>...] [--json]
8336+
$ shopify theme preview --overrides <value> -t <value> [--auth-alias <value>] [-e <value>...] [-j]
83378337
[--json-schema] [--no-color] [--open] [--password <value>] [--path <value>] [--preview-id <value>] [-s <value>]
83388338
[--verbose]
83398339

@@ -8342,6 +8342,10 @@ FLAGS
83428342
The environment to apply to the current command.
83438343
[env: SHOPIFY_FLAG_ENVIRONMENT]
83448344

8345+
-j, --json
8346+
Output the preview URL and identifier as JSON.
8347+
[env: SHOPIFY_FLAG_JSON]
8348+
83458349
-s, --store=<value>
83468350
Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com,
83478351
https://example.myshopify.com).
@@ -8355,10 +8359,6 @@ FLAGS
83558359
Alias of the Shopify account to use for authentication.
83568360
[env: SHOPIFY_FLAG_AUTH_ALIAS]
83578361

8358-
--json
8359-
Output the preview URL and identifier as JSON.
8360-
[env: SHOPIFY_FLAG_JSON]
8361-
83628362
--json-schema
83638363
Print the command's JSON schemas.
83648364
[env: SHOPIFY_FLAG_JSON_SCHEMA]
@@ -8398,6 +8398,31 @@ DESCRIPTION
83988398

83998399
The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id`
84008400
to update an existing preview instead of creating a new one.
8401+
8402+
Output from `--json` conforms to the `ThemePreviewResult` schema.
8403+
8404+
Use `--json-schema` to print the result, error, and event schemas.
8405+
8406+
```json
8407+
{
8408+
"type": "object",
8409+
"properties": {
8410+
"url": {
8411+
"type": "string"
8412+
},
8413+
"preview_identifier": {
8414+
"type": "string"
8415+
}
8416+
},
8417+
"required": [
8418+
"url",
8419+
"preview_identifier"
8420+
],
8421+
"additionalProperties": false,
8422+
"title": "ThemePreviewResult",
8423+
"$schema": "http://json-schema.org/draft-07/schema#"
8424+
}
8425+
```
84018426
```
84028427
84038428
## `shopify theme profile`

‎packages/cli/oclif.manifest.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10796,7 +10796,7 @@
1079610796
"args": {
1079710797
},
1079810798
"customPluginName": "@shopify/theme",
10799-
"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.",
10799+
"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```",
1080010800
"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.",
1080110801
"enableJsonFlag": false,
1080210802
"flags": {
@@ -10819,8 +10819,10 @@
1081910819
},
1082010820
"json": {
1082110821
"allowNo": false,
10822+
"char": "j",
1082210823
"description": "Output the preview URL and identifier as JSON.",
1082310824
"env": "SHOPIFY_FLAG_JSON",
10825+
"hidden": false,
1082410826
"name": "json",
1082510827
"type": "boolean"
1082610828
},

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,6 @@ const commandExceptions = [
6464
'packages/theme/src/cli/commands/theme/init.ts',
6565
'packages/theme/src/cli/commands/theme/metafields/pull.ts',
6666
'packages/theme/src/cli/commands/theme/package.ts',
67-
'packages/theme/src/cli/commands/theme/preview.ts',
6867
'packages/theme/src/cli/commands/theme/profile.ts',
6968

7069
// Streaming commands without a single finite result.

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

Lines changed: 80 additions & 14 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'
23
import {devWithOverrideFile} from '../../services/dev-override.js'
34
import {findOrSelectTheme} from '../../utilities/theme-selector.js'
45
import {ensureThemeStore} from '../../utilities/theme-store.js'
6+
import {openURL} from '@shopify/cli-kit/node/system'
7+
import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events'
8+
import {renderSuccess} from '@shopify/cli-kit/node/ui'
59
import {buildTheme} from '@shopify/cli-kit/node/themes/factories'
610
import {recordEvent} from '@shopify/cli-kit/node/analytics'
711
import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
12+
import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output'
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,70 @@ 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+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
126+
await runWithCommandEventsForCommand(['--json'], () =>
127+
run(['--overrides=/path/to/overrides.json', '--theme=2', '--json']),
128+
)
129+
130+
expect(stdout()).toBe('')
131+
expect(JSON.parse(stderr())).toMatchObject({
132+
type: 'diagnostic',
133+
level: 'info',
134+
message: JSON.stringify(result),
135+
})
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+
})
114147

115-
await run(['--overrides=/path/to/overrides.json', `--theme=${expectedTheme.id}`, '--json'])
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+
})
116154

117-
expect(devWithOverrideFile).toHaveBeenCalledWith(
118-
expect.objectContaining({
119-
themeId: expectedTheme.id.toString(),
120-
json: true,
121-
}),
122-
)
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+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
160+
await expect(run(['--overrides=/path/to/overrides.json', '--theme=2', '--json', '--open'])).rejects.toBe(error)
161+
162+
expect(stdout()).toBe('')
163+
expect(stderr()).toBe('')
164+
})
165+
166+
expect(renderSuccess).not.toHaveBeenCalled()
167+
expect(openURL).not.toHaveBeenCalled()
168+
})
169+
170+
test('keeps browser failures nonfatal and sends a typed warning to stderr', async () => {
171+
const error = new Error('Browser unavailable')
172+
vi.mocked(openURL).mockRejectedValue(error)
173+
174+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
175+
await runWithCommandEventsForCommand(['--json'], () =>
176+
run(['--overrides=/path/to/overrides.json', '--theme=2', '--json', '--open']),
177+
)
178+
179+
const events = stderr()
180+
.trim()
181+
.split('\n')
182+
.map((line) => JSON.parse(line))
183+
expect(events).toMatchObject([
184+
{type: 'diagnostic', level: 'info', message: JSON.stringify(result)},
185+
{type: 'diagnostic', level: 'warning', message: `Failed to open theme preview.\n${error.stack}`},
186+
])
187+
expect(stdout()).toBe('')
188+
})
123189
})
124190
})

‎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)