Skip to content

Commit 188cf16

Browse files
Add typed JSON output to theme preview
1 parent e07dbbd commit 188cf16

13 files changed

Lines changed: 299 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
@@ -8282,15 +8282,6 @@
82828282
"isOptional": true,
82838283
"environmentValue": "SHOPIFY_FLAG_AUTH_ALIAS"
82848284
},
8285-
{
8286-
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
8287-
"syntaxKind": "PropertySignature",
8288-
"name": "--json",
8289-
"value": "''",
8290-
"description": "Output the preview URL and identifier as JSON.",
8291-
"isOptional": true,
8292-
"environmentValue": "SHOPIFY_FLAG_JSON"
8293-
},
82948285
{
82958286
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
82968287
"syntaxKind": "PropertySignature",
@@ -8371,6 +8362,15 @@
83718362
"isOptional": true,
83728363
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
83738364
},
8365+
{
8366+
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
8367+
"syntaxKind": "PropertySignature",
8368+
"name": "-j, --json",
8369+
"value": "''",
8370+
"description": "Output the preview URL and identifier as JSON.",
8371+
"isOptional": true,
8372+
"environmentValue": "SHOPIFY_FLAG_JSON"
8373+
},
83748374
{
83758375
"filePath": "docs-shopify.dev/commands/interfaces/theme-preview.interface.ts",
83768376
"syntaxKind": "PropertySignature",
@@ -8389,7 +8389,7 @@
83898389
"environmentValue": "SHOPIFY_FLAG_THEME_ID"
83908390
}
83918391
],
8392-
"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}"
8392+
"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}"
83938393
}
83948394
},
83958395
"themeprofile": {

‎packages/cli/README.md‎

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8807,7 +8807,7 @@ Applies JSON overrides to a theme and returns a preview URL.
88078807
88088808
```
88098809
USAGE
8810-
$ shopify theme preview --overrides <value> -t <value> [--auth-alias <value>] [-e <value>...] [--json]
8810+
$ shopify theme preview --overrides <value> -t <value> [--auth-alias <value>] [-e <value>...] [-j]
88118811
[--json-schema] [--no-color] [--open] [--password <value>] [--path <value>] [--preview-id <value>] [-s <value>]
88128812
[--verbose]
88138813

@@ -8816,6 +8816,10 @@ FLAGS
88168816
The environment to apply to the current command.
88178817
[env: SHOPIFY_FLAG_ENVIRONMENT]
88188818

8819+
-j, --json
8820+
Output the preview URL and identifier as JSON.
8821+
[env: SHOPIFY_FLAG_JSON]
8822+
88198823
-s, --store=<value>
88208824
Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com,
88218825
https://example.myshopify.com).
@@ -8829,10 +8833,6 @@ FLAGS
88298833
Alias of the Shopify account to use for authentication.
88308834
[env: SHOPIFY_FLAG_AUTH_ALIAS]
88318835

8832-
--json
8833-
Output the preview URL and identifier as JSON.
8834-
[env: SHOPIFY_FLAG_JSON]
8835-
88368836
--json-schema
88378837
Print the command's JSON schemas.
88388838
[env: SHOPIFY_FLAG_JSON_SCHEMA]
@@ -8872,6 +8872,31 @@ DESCRIPTION
88728872

88738873
The command returns a preview URL and a preview identifier. You can reuse the preview identifier with `--preview-id`
88748874
to update an existing preview instead of creating a new one.
8875+
8876+
Use `--json-schema` to print the result, error, and event schemas.
8877+
8878+
Output from `--json` conforms to the `ThemePreviewResult` schema.
8879+
8880+
```json
8881+
{
8882+
"type": "object",
8883+
"properties": {
8884+
"url": {
8885+
"type": "string"
8886+
},
8887+
"preview_identifier": {
8888+
"type": "string"
8889+
}
8890+
},
8891+
"required": [
8892+
"url",
8893+
"preview_identifier"
8894+
],
8895+
"additionalProperties": false,
8896+
"title": "ThemePreviewResult",
8897+
"$schema": "http://json-schema.org/draft-07/schema#"
8898+
}
8899+
```
88758900
```
88768901
88778902
## `shopify theme profile`

‎packages/cli/oclif.manifest.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10791,7 +10791,7 @@
1079110791
"args": {
1079210792
},
1079310793
"customPluginName": "@shopify/theme",
10794-
"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.",
10794+
"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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemePreviewResult` schema.\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```",
1079510795
"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.",
1079610796
"enableJsonFlag": false,
1079710797
"flags": {
@@ -10814,8 +10814,10 @@
1081410814
},
1081510815
"json": {
1081610816
"allowNo": false,
10817+
"char": "j",
1081710818
"description": "Output the preview URL and identifier as JSON.",
1081810819
"env": "SHOPIFY_FLAG_JSON",
10820+
"hidden": false,
1081910821
"name": "json",
1082010822
"type": "boolean"
1082110823
},

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ const commandExceptions = [
4747
'packages/theme/src/cli/commands/theme/init.ts',
4848
'packages/theme/src/cli/commands/theme/metafields/pull.ts',
4949
'packages/theme/src/cli/commands/theme/package.ts',
50-
'packages/theme/src/cli/commands/theme/preview.ts',
5150
'packages/theme/src/cli/commands/theme/profile.ts',
5251

5352
// Streaming commands without a single finite result.

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

Lines changed: 75 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,65 @@ 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('writes the JSON result to stdout 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(JSON.parse(stdout())).toEqual(result)
131+
expect(stderr()).toBe('')
132+
})
133+
expect(renderSuccess).not.toHaveBeenCalled()
134+
expect(openURL).not.toHaveBeenCalled()
135+
expect(devWithOverrideFile).toHaveBeenCalledWith({
136+
adminSession,
137+
overrideJson: '/path/to/overrides.json',
138+
themeId: '2',
139+
previewIdentifier: undefined,
140+
password: undefined,
141+
})
142+
})
114143

115-
await run(['--overrides=/path/to/overrides.json', `--theme=${expectedTheme.id}`, '--json'])
144+
test('exposes its result schema in help', () => {
145+
expect(Preview.jsonOutputSchema).toBe(themePreviewJsonOutputSchema)
146+
expect(Preview.description).toContain('ThemePreviewResult')
147+
expect(Preview.description).toContain('preview_identifier')
148+
expect(Preview.flags.json.env).toBe('SHOPIFY_FLAG_JSON')
149+
})
116150

117-
expect(devWithOverrideFile).toHaveBeenCalledWith(
118-
expect.objectContaining({
119-
themeId: expectedTheme.id.toString(),
120-
json: true,
121-
}),
122-
)
151+
test('propagates failures without printing a success result or opening a browser', async () => {
152+
const error = new Error('Failed to parse override file')
153+
vi.mocked(devWithOverrideFile).mockRejectedValue(error)
154+
155+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
156+
await expect(run(['--overrides=/path/to/overrides.json', '--theme=2', '--json', '--open'])).rejects.toBe(error)
157+
158+
expect(stdout()).toBe('')
159+
expect(stderr()).toBe('')
160+
})
161+
162+
expect(renderSuccess).not.toHaveBeenCalled()
163+
expect(openURL).not.toHaveBeenCalled()
164+
})
165+
166+
test('keeps browser failures nonfatal and sends a typed warning to stderr', async () => {
167+
const error = new Error('Browser unavailable')
168+
vi.mocked(openURL).mockRejectedValue(error)
169+
170+
await withCapturedStandardStreams(async ({stdout, stderr}) => {
171+
await runWithCommandEventsForCommand(['--json'], () =>
172+
run(['--overrides=/path/to/overrides.json', '--theme=2', '--json', '--open']),
173+
)
174+
175+
const events = stderr()
176+
.trim()
177+
.split('\n')
178+
.map((line) => JSON.parse(line))
179+
expect(events).toMatchObject([
180+
{type: 'diagnostic', level: 'warning', message: `Failed to open theme preview.\n${error.stack}`},
181+
])
182+
expect(JSON.parse(stdout())).toEqual(result)
183+
})
123184
})
124185
})

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