From 44ce1666a3cc040337f4ab6e3b7147b1fe9e4fdf Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Thu, 24 Sep 2026 11:16:07 +0200 Subject: [PATCH 1/5] Add typed JSON output to theme open --- packages/cli/oclif.manifest.json | 11 ++- .../rules/json-output-command-exceptions.js | 1 - .../theme/src/cli/commands/theme/open.test.ts | 89 +++++++++++++++++++ packages/theme/src/cli/commands/theme/open.ts | 14 ++- packages/theme/src/cli/services/open.test.ts | 66 +++----------- packages/theme/src/cli/services/open.ts | 30 +------ .../src/cli/services/open/result.test.ts | 83 +++++++++++++++++ .../theme/src/cli/services/open/result.ts | 27 ++++++ packages/theme/src/cli/services/open/types.ts | 23 +++++ 9 files changed, 262 insertions(+), 82 deletions(-) create mode 100644 packages/theme/src/cli/commands/theme/open.test.ts create mode 100644 packages/theme/src/cli/services/open/result.test.ts create mode 100644 packages/theme/src/cli/services/open/result.ts create mode 100644 packages/theme/src/cli/services/open/types.ts diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index b385e252ebc..fc185ada70b 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -11675,7 +11675,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "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.", + "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeOpenResult` schema.\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```", "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.", "enableJsonFlag": false, "flags": { @@ -11712,6 +11712,15 @@ "name": "environment", "type": "option" }, + "json": { + "allowNo": false, + "char": "j", + "description": "Output the result as JSON. Automatically disables color output.", + "env": "SHOPIFY_FLAG_JSON", + "hidden": false, + "name": "json", + "type": "boolean" + }, "json-schema": { "allowNo": false, "description": "Print the command's JSON schemas.", diff --git a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js index 126164f886e..37e3b9092d6 100644 --- a/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js +++ b/packages/eslint-plugin-cli/rules/json-output-command-exceptions.js @@ -30,7 +30,6 @@ const commandExceptions = [ 'packages/theme/src/cli/commands/theme/duplicate.ts', 'packages/theme/src/cli/commands/theme/init.ts', 'packages/theme/src/cli/commands/theme/metafields/pull.ts', - 'packages/theme/src/cli/commands/theme/open.ts', 'packages/theme/src/cli/commands/theme/package.ts', 'packages/theme/src/cli/commands/theme/preview.ts', 'packages/theme/src/cli/commands/theme/profile.ts', diff --git a/packages/theme/src/cli/commands/theme/open.test.ts b/packages/theme/src/cli/commands/theme/open.test.ts new file mode 100644 index 00000000000..6b433065c5f --- /dev/null +++ b/packages/theme/src/cli/commands/theme/open.test.ts @@ -0,0 +1,89 @@ +import Open from './open.js' +import {open} from '../../services/open.js' +import {themeOpenJsonOutputSchema, type ThemeOpenResult} from '../../services/open/types.js' +import {ensureThemeStore} from '../../utilities/theme-store.js' +import {Config} from '@oclif/core' +import {expect, test, vi} from 'vitest' +import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' +import {openURL} from '@shopify/cli-kit/node/system' +import {renderInfo} from '@shopify/cli-kit/node/ui' +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' + +vi.mock('../../services/open.js') +vi.mock('../../utilities/theme-store.js') +vi.mock('@shopify/cli-kit/node/session') +vi.mock('@shopify/cli-kit/node/system', async (importOriginal) => ({ + ...(await importOriginal()), + openURL: vi.fn(), +})) +vi.mock('@shopify/cli-kit/node/ui') +vi.mock('@shopify/cli-kit/node/environments') +vi.mock('@shopify/cli-kit/node/analytics') +vi.mock('@shopify/cli-kit/node/metadata') + +const session = {token: 'token', storeFqdn: 'store.myshopify.com'} +const result: ThemeOpenResult = { + theme: {id: 1, name: 'my theme', role: 'live', processing: false, createdAtRuntime: false}, + preview_url: 'https://store.myshopify.com?preview_theme_id=1', + editor_url: 'https://store.myshopify.com/admin/themes/1/editor', +} + +async function run(argv: string[]) { + const config = new Config({root: __dirname}) + await config.load() + vi.mocked(ensureThemeStore).mockReturnValue(session.storeFqdn) + vi.mocked(ensureAuthenticatedThemes).mockResolvedValue(session) + await new Open(['--store=store.myshopify.com', ...argv], config).run() +} + +test('exposes the result schema and JSON flag in help', () => { + expect(Open.jsonOutputSchema).toBe(themeOpenJsonOutputSchema) + expect(Open.flags.json).toBeDefined() + expect(Open.description).toContain('ThemeOpenResult') + expect(Open.description).toContain('preview_url') +}) + +test.each([false, true])('preserves browser selection with editor=%s', async (editor) => { + vi.mocked(open).mockResolvedValue(result) + + await run(['--theme=1', ...(editor ? ['--editor'] : [])]) + + expect(renderInfo).toHaveBeenCalled() + expect(openURL).toHaveBeenCalledWith(editor ? result.editor_url : result.preview_url) +}) + +test('writes one JSON result', async () => { + vi.mocked(open).mockResolvedValue(result) + + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await run(['--theme=1', '--json']) + + expect(JSON.parse(stdout())).toEqual(result) + expect(stderr()).toBe('') + }) + expect(renderInfo).not.toHaveBeenCalled() + expect(openURL).toHaveBeenCalledWith(result.preview_url) + expect(open).toHaveBeenCalledWith(session, expect.objectContaining({theme: '1', json: true})) +}) + +test('propagates selection errors without opening the browser or rendering a result', async () => { + const error = new Error('Theme not found') + vi.mocked(open).mockRejectedValue(error) + + await expect(run(['--theme=1', '--json'])).rejects.toBe(error) + + expect(openURL).not.toHaveBeenCalled() + expect(renderInfo).not.toHaveBeenCalled() +}) + +test('preserves browser failures after writing the result', async () => { + vi.mocked(open).mockResolvedValue(result) + const error = new Error('Browser unavailable') + vi.mocked(openURL).mockRejectedValue(error) + + await withCapturedStandardStreams(async ({stdout}) => { + await expect(run(['--theme=1', '--json'])).rejects.toBe(error) + + expect(JSON.parse(stdout())).toEqual(result) + }) +}) diff --git a/packages/theme/src/cli/commands/theme/open.ts b/packages/theme/src/cli/commands/theme/open.ts index e2bbd3d2016..900e35ac7bf 100644 --- a/packages/theme/src/cli/commands/theme/open.ts +++ b/packages/theme/src/cli/commands/theme/open.ts @@ -1,14 +1,21 @@ import {open} from '../../services/open.js' +import {renderThemeOpenResult} from '../../services/open/result.js' +import {themeOpenJsonOutputSchema} from '../../services/open/types.js' import {themeFlags} from '../../flags.js' import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.js' import {Flags} from '@oclif/core' -import {globalFlags} from '@shopify/cli-kit/node/cli' +import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' +import {openURL} from '@shopify/cli-kit/node/system' import {AdminSession} from '@shopify/cli-kit/node/session' import {InferredFlags} from '@oclif/core/interfaces' import type {NonTTYFlagRequirement} from '@shopify/cli-kit/node/base-command' type OpenFlags = InferredFlags export default class Open extends ThemeCommand { + static get jsonOutputSchema() { + return themeOpenJsonOutputSchema + } + static summary = 'Opens the preview of your remote theme.' 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 { static flags = { ...globalFlags, + ...jsonFlag, ...themeFlags, development: Flags.boolean({ char: 'd', @@ -55,6 +63,8 @@ export default class Open extends ThemeCommand { } async command(flags: OpenFlags, adminSession: AdminSession) { - await open(adminSession, flags) + const result = await open(adminSession, flags) + renderThemeOpenResult(result, flags.json ? 'json' : 'text') + await openURL(flags.editor ? result.editor_url : result.preview_url) } } diff --git a/packages/theme/src/cli/services/open.test.ts b/packages/theme/src/cli/services/open.test.ts index 92beee7b16f..13c009aa37c 100644 --- a/packages/theme/src/cli/services/open.test.ts +++ b/packages/theme/src/cli/services/open.test.ts @@ -40,63 +40,25 @@ const options = { } describe('open', () => { - test('opens the preview URL', async () => { - // Given + test('returns theme details and URLs without opening a browser or rendering', async () => { vi.mocked(findOrSelectTheme).mockResolvedValue(theme) - // When - await open(session, options) - - // Then - expect(openURL).toBeCalledWith('https://my-shop.myshopify.com?preview_theme_id=1') - }) - - test('opens the editor URL with `editor` flag', async () => { - // Given - vi.mocked(findOrSelectTheme).mockResolvedValue(theme) - - // When - await open(session, {...options, editor: true}) - - // Then - expect(openURL).toBeCalledWith('https://my-shop.myshopify.com/admin/themes/1/editor') + await expect(open(session, options)).resolves.toEqual({ + theme, + preview_url: 'https://my-shop.myshopify.com?preview_theme_id=1', + editor_url: 'https://my-shop.myshopify.com/admin/themes/1/editor', + }) + expect(openURL).not.toHaveBeenCalled() + expect(renderInfo).not.toHaveBeenCalled() }) - test('renders the theme links', async () => { - // Given - vi.spyOn(DevelopmentThemeManager.prototype, 'fetch').mockResolvedValue(theme) - vi.mocked(findOrSelectTheme).mockResolvedValue(theme) + test('propagates theme selection failures', async () => { + const error = new Error('Theme not found') + vi.mocked(findOrSelectTheme).mockRejectedValue(error) - // When - await open(session, options) - - // Then - expect(renderInfo).toBeCalledWith({ - body: [ - 'Preview information for theme', - "'my theme'", - {subdued: '(#1)'}, - '\n\n', - { - list: { - items: [ - { - link: { - label: 'Preview your theme', - url: 'https://my-shop.myshopify.com?preview_theme_id=1', - }, - }, - { - link: { - label: 'Customize your theme at the theme editor', - url: 'https://my-shop.myshopify.com/admin/themes/1/editor', - }, - }, - ], - }, - }, - ], - }) + await expect(open(session, options)).rejects.toBe(error) + expect(openURL).not.toHaveBeenCalled() + expect(renderInfo).not.toHaveBeenCalled() }) describe('findOrSelectTheme', () => { diff --git a/packages/theme/src/cli/services/open.ts b/packages/theme/src/cli/services/open.ts index dfa732060f6..8ed2de8843a 100644 --- a/packages/theme/src/cli/services/open.ts +++ b/packages/theme/src/cli/services/open.ts @@ -1,15 +1,13 @@ +import {type ThemeOpenResult} from './open/types.js' import {DevelopmentThemeManager} from '../utilities/development-theme-manager.js' -import {themeComponent} from '../utilities/theme-ui.js' import {findOrSelectTheme} from '../utilities/theme-selector.js' import {themeEditorUrl, themePreviewUrl} from '@shopify/cli-kit/node/themes/urls' -import {openURL} from '@shopify/cli-kit/node/system' -import {renderInfo} from '@shopify/cli-kit/node/ui' import {AdminSession} from '@shopify/cli-kit/node/session' export async function open( adminSession: AdminSession, - options: {development: boolean; live: boolean; editor: boolean; theme: string | undefined}, -) { + options: {development: boolean; live: boolean; theme: string | undefined}, +): Promise { const developmentThemeManager = new DevelopmentThemeManager(adminSession) const developmentTheme = ( await (options.development ? developmentThemeManager.find() : developmentThemeManager.fetch()) @@ -25,25 +23,5 @@ export async function open( const previewUrl = themePreviewUrl(theme, adminSession) const editorUrl = themeEditorUrl(theme, adminSession) - renderInfo({ - body: [ - `Preview information for theme`, - ...themeComponent(theme), - '\n\n', - { - list: { - items: [ - {link: {label: 'Preview your theme', url: previewUrl}}, - {link: {label: 'Customize your theme at the theme editor', url: editorUrl}}, - ], - }, - }, - ], - }) - - if (options.editor) { - await openURL(editorUrl) - } else { - await openURL(previewUrl) - } + return {theme, preview_url: previewUrl, editor_url: editorUrl} } diff --git a/packages/theme/src/cli/services/open/result.test.ts b/packages/theme/src/cli/services/open/result.test.ts new file mode 100644 index 00000000000..1d2d75430c5 --- /dev/null +++ b/packages/theme/src/cli/services/open/result.test.ts @@ -0,0 +1,83 @@ +import {renderThemeOpenResult} from './result.js' +import {themeOpenJsonOutputSchema, type ThemeOpenResult} from './types.js' +import {expect, test, vi} from 'vitest' +import {renderInfo} from '@shopify/cli-kit/node/ui' +import {runWithCommandEventsForCommand} from '@shopify/cli-kit/node/command-events' +import {outputWarn} from '@shopify/cli-kit/node/output' +import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' + +vi.mock('@shopify/cli-kit/node/ui') + +const result: ThemeOpenResult = { + theme: {id: 1, name: 'my theme', role: 'unpublished', processing: false, createdAtRuntime: false}, + preview_url: 'https://my-shop.myshopify.com?preview_theme_id=1', + editor_url: 'https://my-shop.myshopify.com/admin/themes/1/editor', +} + +test('writes one JSON document to stdout and diagnostics to stderr', async () => { + await withCapturedStandardStreams(async ({stdout, stderr}) => { + await runWithCommandEventsForCommand(['--json'], () => { + outputWarn('A diagnostic') + renderThemeOpenResult(result, 'json') + }) + + expect(stdout()).toBe(`${themeOpenJsonOutputSchema.encode(result)}\n`) + expect(JSON.parse(stdout())).toEqual(result) + expect(JSON.parse(stderr())).toMatchObject({type: 'diagnostic', level: 'warning', message: 'A diagnostic'}) + }) + expect(renderInfo).not.toHaveBeenCalled() +}) + +test('preserves false values and omits an absent theme source', () => { + expect(JSON.parse(themeOpenJsonOutputSchema.encode(result))).toEqual(result) + expect(JSON.parse(themeOpenJsonOutputSchema.encode(result)).theme).not.toHaveProperty('src') +}) + +test('includes an available theme source', () => { + const withSource = {...result, theme: {...result.theme, src: 'https://example.com/theme.zip'}} + expect(JSON.parse(themeOpenJsonOutputSchema.encode(withSource))).toEqual(withSource) +}) + +test.each([ + {theme: {...result.theme, id: '1'}}, + {theme: {...result.theme, processing: null}}, + {preview_url: null}, + {editor_url: 1}, +])('rejects invalid result fields %j', (invalid) => { + expect(() => themeOpenJsonOutputSchema.validate({...result, ...invalid})).toThrow() +}) + +test('renders the theme links', () => { + // Given + + // When + renderThemeOpenResult(result, 'text') + + // Then + expect(renderInfo).toBeCalledWith({ + body: [ + 'Preview information for theme', + "'my theme'", + {subdued: '(#1)'}, + '\n\n', + { + list: { + items: [ + { + link: { + label: 'Preview your theme', + url: 'https://my-shop.myshopify.com?preview_theme_id=1', + }, + }, + { + link: { + label: 'Customize your theme at the theme editor', + url: 'https://my-shop.myshopify.com/admin/themes/1/editor', + }, + }, + ], + }, + }, + ], + }) +}) diff --git a/packages/theme/src/cli/services/open/result.ts b/packages/theme/src/cli/services/open/result.ts new file mode 100644 index 00000000000..3f0d8d43f25 --- /dev/null +++ b/packages/theme/src/cli/services/open/result.ts @@ -0,0 +1,27 @@ +import {themeOpenJsonOutputSchema, type ThemeOpenResult} from './types.js' +import {themeComponent} from '../../utilities/theme-ui.js' +import {outputResult} from '@shopify/cli-kit/node/output' +import {renderInfo} from '@shopify/cli-kit/node/ui' + +export function renderThemeOpenResult(result: ThemeOpenResult, format: 'text' | 'json'): void { + if (format === 'json') { + outputResult(themeOpenJsonOutputSchema.encode(result)) + return + } + + renderInfo({ + body: [ + 'Preview information for theme', + ...themeComponent(result.theme), + '\n\n', + { + list: { + items: [ + {link: {label: 'Preview your theme', url: result.preview_url}}, + {link: {label: 'Customize your theme at the theme editor', url: result.editor_url}}, + ], + }, + }, + ], + }) +} diff --git a/packages/theme/src/cli/services/open/types.ts b/packages/theme/src/cli/services/open/types.ts new file mode 100644 index 00000000000..ce0e25c7faa --- /dev/null +++ b/packages/theme/src/cli/services/open/types.ts @@ -0,0 +1,23 @@ +import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import {zod} from '@shopify/cli-kit/node/schema' + +const ThemeSchema = zod.object({ + id: zod.number(), + name: zod.string(), + createdAtRuntime: zod.boolean(), + processing: zod.boolean(), + role: zod.string(), + src: zod.string().optional(), +}) + +export const themeOpenJsonOutputSchema = defineJsonOutputSchema({ + name: 'ThemeOpenResult', + schema: zod.object({ + theme: ThemeSchema, + preview_url: zod.string(), + editor_url: zod.string(), + }), + definitions: {Theme: ThemeSchema}, +}) + +export type ThemeOpenResult = InferJsonOutputSchema From 2eb8ab6f8ae8bdbc7d443f65c1548b792126102d Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Tue, 6 Oct 2026 13:59:02 +0200 Subject: [PATCH 2/5] Project theme open JSON resources before emitting a successful result --- .changeset/theme-open-json-contract.md | 5 + packages/cli/README.md | 310 +++++++++++++++++- packages/cli/oclif.manifest.json | 2 +- .../theme/src/cli/commands/theme/open.test.ts | 21 +- packages/theme/src/cli/commands/theme/open.ts | 6 +- .../src/cli/services/open/result.test.ts | 35 +- packages/theme/src/cli/services/open/types.ts | 44 ++- 7 files changed, 396 insertions(+), 27 deletions(-) create mode 100644 .changeset/theme-open-json-contract.md diff --git a/.changeset/theme-open-json-contract.md b/.changeset/theme-open-json-contract.md new file mode 100644 index 00000000000..91e90f10ba8 --- /dev/null +++ b/.changeset/theme-open-json-contract.md @@ -0,0 +1,5 @@ +--- +'@shopify/cli': major +--- + +Use string IDs and camelCase links inside `theme open --json` resources, and emit only a fatal error when opening the browser fails. diff --git a/packages/cli/README.md b/packages/cli/README.md index 2a5b437f9a5..d2b7c704ebb 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -11695,7 +11695,7 @@ Opens the preview of your remote theme. ``` USAGE - $ shopify theme open [--auth-alias ] [-d] [-E] [-e ...] [--json-schema] [-l] [--no-color] + $ shopify theme open [--auth-alias ] [-d] [-E] [-e ...] [-j] [--json-schema] [-l] [--no-color] [--no-input] [--password ] [--path ] [-s ] [-t ] [--verbose] FLAGS @@ -11711,6 +11711,10 @@ FLAGS The environment to apply to the current command. [env: SHOPIFY_FLAG_ENVIRONMENT] + -j, --json + Output the result as JSON. Automatically disables color output. + [env: SHOPIFY_FLAG_JSON] + -l, --live Open your live (published) theme. Use --development, --live, or --theme in non-interactive environments. [env: SHOPIFY_FLAG_LIVE] @@ -11764,6 +11768,310 @@ DESCRIPTION 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. + + Use `--json-schema` to print the result, error, and event schemas. + + Output from `--json` conforms to the `ThemeOpenResult` schema. + + ```json + { + "anyOf": [ + { + "$ref": "#/definitions/ThemeEnvironment/anyOf/0/properties/result" + }, + { + "type": "object", + "properties": { + "environments": { + "type": "array", + "items": { + "$ref": "#/definitions/ThemeEnvironment" + } + } + }, + "required": [ + "environments" + ], + "additionalProperties": false + } + ], + "title": "ThemeOpenResult", + "definitions": { + "Theme": { + "type": "object", + "properties": { + "id": { + "type": "string", + "pattern": "^\\d+$", + "description": "The decimal Online Store theme ID, not a Shopify GID." + }, + "name": { + "type": "string" + }, + "role": { + "type": "string", + "description": "The upstream theme role; known values include main, unpublished, and development." + } + }, + "required": [ + "id", + "name", + "role" + ], + "additionalProperties": false + }, + "ThemeEnvironment": { + "anyOf": [ + { + "type": "object", + "properties": { + "environment": { + "type": "string" + }, + "result": { + "anyOf": [ + { + "type": "object", + "properties": { + "theme": { + "type": "object", + "properties": { + "id": { + "$ref": "#/definitions/Theme/properties/id" + }, + "name": { + "$ref": "#/definitions/Theme/properties/name" + }, + "role": { + "$ref": "#/definitions/Theme/properties/role" + }, + "storeDomain": { + "anyOf": [ + { + "type": "string", + "pattern": "^[a-z0-9][a-z0-9-]*\\.myshopify\\.com$" + }, + { + "type": "null" + } + ] + }, + "previewUrl": { + "type": "string", + "format": "uri" + }, + "editorUrl": { + "type": "string", + "format": "uri" + }, + "processing": { + "type": "boolean" + }, + "sourceUrl": { + "anyOf": [ + { + "type": "string", + "format": "uri" + }, + { + "type": "null" + } + ] + } + }, + "required": [ + "id", + "name", + "role", + "storeDomain", + "previewUrl", + "editorUrl", + "processing", + "sourceUrl" + ], + "additionalProperties": false + } + }, + "required": [ + "theme" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "status": { + "type": "string", + "const": "cancelled" + } + }, + "required": [ + "status" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "environment", + "result" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "environment": { + "type": "string" + }, + "error": { + "anyOf": [ + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "abort" + }, + "message": { + "type": "string" + }, + "tryMessage": { + "type": "string" + }, + "nextSteps": { + "type": "array", + "items": { + "type": "string" + } + }, + "customSections": { + "type": "array", + "items": { + "type": "object", + "properties": { + "title": { + "type": "string" + }, + "body": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "array", + "items": { + "type": "array", + "items": { + "type": "string" + } + } + } + ] + } + }, + "required": [ + "body" + ], + "additionalProperties": false + } + }, + "details": {} + }, + "required": [ + "type", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "bug" + }, + "message": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message" + }, + "tryMessage": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage" + }, + "nextSteps": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps" + }, + "customSections": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections" + }, + "details": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details" + }, + "stack": { + "type": "string" + } + }, + "required": [ + "type", + "message" + ], + "additionalProperties": false + }, + { + "type": "object", + "properties": { + "type": { + "type": "string", + "const": "external" + }, + "message": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message" + }, + "tryMessage": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage" + }, + "nextSteps": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps" + }, + "customSections": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections" + }, + "details": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details" + }, + "command": { + "type": "string" + }, + "args": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [ + "type", + "message", + "command", + "args" + ], + "additionalProperties": false + } + ] + } + }, + "required": [ + "environment", + "error" + ], + "additionalProperties": false + } + ] + } + }, + "$schema": "http://json-schema.org/draft-07/schema#" + } + ``` ``` ## `shopify theme package` diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index fc185ada70b..df270de8d41 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -11675,7 +11675,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeOpenResult` schema.\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```", + "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeOpenResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemeOpenResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include main, unpublished, and development.\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"$ref\": \"#/definitions/Theme/properties/id\"\n },\n \"name\": {\n \"$ref\": \"#/definitions/Theme/properties/name\"\n },\n \"role\": {\n \"$ref\": \"#/definitions/Theme/properties/role\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"previewUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"editorUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"previewUrl\",\n \"editorUrl\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {}\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "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.", "enableJsonFlag": false, "flags": { diff --git a/packages/theme/src/cli/commands/theme/open.test.ts b/packages/theme/src/cli/commands/theme/open.test.ts index 6b433065c5f..8dbd97c0351 100644 --- a/packages/theme/src/cli/commands/theme/open.test.ts +++ b/packages/theme/src/cli/commands/theme/open.test.ts @@ -36,11 +36,24 @@ async function run(argv: string[]) { await new Open(['--store=store.myshopify.com', ...argv], config).run() } +const publicResult = { + theme: { + id: '1', + name: 'my theme', + role: 'live', + storeDomain: 'store.myshopify.com', + previewUrl: 'https://store.myshopify.com?preview_theme_id=1', + editorUrl: 'https://store.myshopify.com/admin/themes/1/editor', + processing: false, + sourceUrl: null, + }, +} + test('exposes the result schema and JSON flag in help', () => { expect(Open.jsonOutputSchema).toBe(themeOpenJsonOutputSchema) expect(Open.flags.json).toBeDefined() expect(Open.description).toContain('ThemeOpenResult') - expect(Open.description).toContain('preview_url') + expect(Open.description).toContain('previewUrl') }) test.each([false, true])('preserves browser selection with editor=%s', async (editor) => { @@ -58,7 +71,7 @@ test('writes one JSON result', async () => { await withCapturedStandardStreams(async ({stdout, stderr}) => { await run(['--theme=1', '--json']) - expect(JSON.parse(stdout())).toEqual(result) + expect(JSON.parse(stdout())).toEqual(publicResult) expect(stderr()).toBe('') }) expect(renderInfo).not.toHaveBeenCalled() @@ -76,7 +89,7 @@ test('propagates selection errors without opening the browser or rendering a res expect(renderInfo).not.toHaveBeenCalled() }) -test('preserves browser failures after writing the result', async () => { +test('propagates browser failures before emitting the JSON result', async () => { vi.mocked(open).mockResolvedValue(result) const error = new Error('Browser unavailable') vi.mocked(openURL).mockRejectedValue(error) @@ -84,6 +97,6 @@ test('preserves browser failures after writing the result', async () => { await withCapturedStandardStreams(async ({stdout}) => { await expect(run(['--theme=1', '--json'])).rejects.toBe(error) - expect(JSON.parse(stdout())).toEqual(result) + expect(stdout()).toBe('') }) }) diff --git a/packages/theme/src/cli/commands/theme/open.ts b/packages/theme/src/cli/commands/theme/open.ts index 900e35ac7bf..a477ecdd0f4 100644 --- a/packages/theme/src/cli/commands/theme/open.ts +++ b/packages/theme/src/cli/commands/theme/open.ts @@ -62,9 +62,11 @@ export default class Open extends ThemeCommand { return [{flags: ['theme', 'development', 'live']}] } - async command(flags: OpenFlags, adminSession: AdminSession) { + async command(flags: OpenFlags, adminSession: AdminSession, multiEnvironment = false) { const result = await open(adminSession, flags) - renderThemeOpenResult(result, flags.json ? 'json' : 'text') + if (!flags.json) renderThemeOpenResult(result, 'text') await openURL(flags.editor ? result.editor_url : result.preview_url) + if (flags.json && !multiEnvironment) renderThemeOpenResult(result, 'json') + return result } } diff --git a/packages/theme/src/cli/services/open/result.test.ts b/packages/theme/src/cli/services/open/result.test.ts index 1d2d75430c5..f710bd733eb 100644 --- a/packages/theme/src/cli/services/open/result.test.ts +++ b/packages/theme/src/cli/services/open/result.test.ts @@ -14,6 +14,19 @@ const result: ThemeOpenResult = { editor_url: 'https://my-shop.myshopify.com/admin/themes/1/editor', } +const publicResult = { + theme: { + id: '1', + name: 'my theme', + role: 'unpublished', + storeDomain: 'my-shop.myshopify.com', + previewUrl: 'https://my-shop.myshopify.com?preview_theme_id=1', + editorUrl: 'https://my-shop.myshopify.com/admin/themes/1/editor', + processing: false, + sourceUrl: null, + }, +} + test('writes one JSON document to stdout and diagnostics to stderr', async () => { await withCapturedStandardStreams(async ({stdout, stderr}) => { await runWithCommandEventsForCommand(['--json'], () => { @@ -22,29 +35,31 @@ test('writes one JSON document to stdout and diagnostics to stderr', async () => }) expect(stdout()).toBe(`${themeOpenJsonOutputSchema.encode(result)}\n`) - expect(JSON.parse(stdout())).toEqual(result) + expect(JSON.parse(stdout())).toEqual(publicResult) expect(JSON.parse(stderr())).toMatchObject({type: 'diagnostic', level: 'warning', message: 'A diagnostic'}) }) expect(renderInfo).not.toHaveBeenCalled() }) -test('preserves false values and omits an absent theme source', () => { - expect(JSON.parse(themeOpenJsonOutputSchema.encode(result))).toEqual(result) - expect(JSON.parse(themeOpenJsonOutputSchema.encode(result)).theme).not.toHaveProperty('src') +test('preserves false values and reports an unavailable theme source as null', () => { + expect(JSON.parse(themeOpenJsonOutputSchema.encode(result))).toEqual(publicResult) + expect(JSON.parse(themeOpenJsonOutputSchema.encode(result)).theme.sourceUrl).toBeNull() }) test('includes an available theme source', () => { const withSource = {...result, theme: {...result.theme, src: 'https://example.com/theme.zip'}} - expect(JSON.parse(themeOpenJsonOutputSchema.encode(withSource))).toEqual(withSource) + expect(JSON.parse(themeOpenJsonOutputSchema.encode(withSource))).toEqual({ + theme: {...publicResult.theme, sourceUrl: 'https://example.com/theme.zip'}, + }) }) test.each([ - {theme: {...result.theme, id: '1'}}, - {theme: {...result.theme, processing: null}}, - {preview_url: null}, - {editor_url: 1}, + {theme: {...publicResult.theme, id: 1}}, + {theme: {...publicResult.theme, processing: null}}, + {theme: {...publicResult.theme, previewUrl: null}}, + {theme: {...publicResult.theme, editorUrl: 1}}, ])('rejects invalid result fields %j', (invalid) => { - expect(() => themeOpenJsonOutputSchema.validate({...result, ...invalid})).toThrow() + expect(() => themeOpenJsonOutputSchema.validate({...publicResult, ...invalid})).toThrow() }) test('renders the theme links', () => { diff --git a/packages/theme/src/cli/services/open/types.ts b/packages/theme/src/cli/services/open/types.ts index ce0e25c7faa..82c784c55bd 100644 --- a/packages/theme/src/cli/services/open/types.ts +++ b/packages/theme/src/cli/services/open/types.ts @@ -1,4 +1,10 @@ -import {defineJsonOutputSchema, type InferJsonOutputSchema} from '@shopify/cli-kit/node/json-output-schema' +import { + defineThemeJsonOutputSchema, + ThemeSchema as PublicThemeSchema, + StoreDomainSchema, + projectTheme, + storeDomain, +} from '../json-output/schema.js' import {zod} from '@shopify/cli-kit/node/schema' const ThemeSchema = zod.object({ @@ -10,14 +16,34 @@ const ThemeSchema = zod.object({ src: zod.string().optional(), }) -export const themeOpenJsonOutputSchema = defineJsonOutputSchema({ +const ThemeOpenServiceSchema = zod.object({theme: ThemeSchema, preview_url: zod.string(), editor_url: zod.string()}) +export const themeOpenJsonOutputSchema = defineThemeJsonOutputSchema({ name: 'ThemeOpenResult', - schema: zod.object({ - theme: ThemeSchema, - preview_url: zod.string(), - editor_url: zod.string(), - }), - definitions: {Theme: ThemeSchema}, + schema: zod + .object({ + theme: PublicThemeSchema.extend({ + storeDomain: StoreDomainSchema, + previewUrl: zod.string().url(), + editorUrl: zod.string().url(), + processing: zod.boolean(), + sourceUrl: zod.string().url().nullable(), + }).strict(), + }) + .strict(), + definitions: {Theme: PublicThemeSchema}, + project(value) { + const result = ThemeOpenServiceSchema.parse(value) + return { + theme: { + ...projectTheme(result.theme), + storeDomain: storeDomain(new URL(result.preview_url).hostname), + previewUrl: result.preview_url, + editorUrl: result.editor_url, + processing: result.theme.processing, + sourceUrl: result.theme.src ?? null, + }, + } + }, }) -export type ThemeOpenResult = InferJsonOutputSchema +export type ThemeOpenResult = zod.infer From ae4ffd8f739eb06792419b6a93f8456ada337077 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Tue, 6 Oct 2026 14:16:41 +0200 Subject: [PATCH 3/5] Refresh theme open JSON schema documentation --- packages/cli/README.md | 2 +- packages/cli/oclif.manifest.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index d2b7c704ebb..6cebbd9a30e 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -11810,7 +11810,7 @@ DESCRIPTION }, "role": { "type": "string", - "description": "The upstream theme role; known values include main, unpublished, and development." + "description": "The upstream theme role; known values include live, unpublished, and development." } }, "required": [ diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index df270de8d41..2ed4e5fcd10 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -11675,7 +11675,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeOpenResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemeOpenResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include main, unpublished, and development.\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"$ref\": \"#/definitions/Theme/properties/id\"\n },\n \"name\": {\n \"$ref\": \"#/definitions/Theme/properties/name\"\n },\n \"role\": {\n \"$ref\": \"#/definitions/Theme/properties/role\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"previewUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"editorUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"previewUrl\",\n \"editorUrl\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {}\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", + "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeOpenResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemeOpenResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include live, unpublished, and development.\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"$ref\": \"#/definitions/Theme/properties/id\"\n },\n \"name\": {\n \"$ref\": \"#/definitions/Theme/properties/name\"\n },\n \"role\": {\n \"$ref\": \"#/definitions/Theme/properties/role\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"previewUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"editorUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"previewUrl\",\n \"editorUrl\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {}\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "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.", "enableJsonFlag": false, "flags": { From beb956420f5e841572e80ac8db5b029ee4f44bdb Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Tue, 6 Oct 2026 14:17:12 +0200 Subject: [PATCH 4/5] Refresh generated theme open command docs --- .../generated/generated_docs_data_v2.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index 2191cb75c30..8005ad5320d 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -9500,6 +9500,15 @@ "isOptional": true, "environmentValue": "SHOPIFY_FLAG_ENVIRONMENT" }, + { + "filePath": "docs-shopify.dev/commands/interfaces/theme-open.interface.ts", + "syntaxKind": "PropertySignature", + "name": "-j, --json", + "value": "''", + "description": "Output the result as JSON. Automatically disables color output.", + "isOptional": true, + "environmentValue": "SHOPIFY_FLAG_JSON" + }, { "filePath": "docs-shopify.dev/commands/interfaces/theme-open.interface.ts", "syntaxKind": "PropertySignature", @@ -9528,7 +9537,7 @@ "environmentValue": "SHOPIFY_FLAG_THEME_ID" } ], - "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 '?: 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 '?: 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 * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password '?: 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 '?: 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 '?: 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 '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" + "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 '?: 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 '?: 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 * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password '?: 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 '?: 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 '?: 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 '?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}" } }, "themepackage": { From 1b7baea3aebfb36ab22c53968b27db5f219f1bc4 Mon Sep 17 00:00:00 2001 From: Gonzalo Riestra Date: Thu, 8 Oct 2026 10:35:42 +0200 Subject: [PATCH 5/5] Report unsuccessful theme browser launches --- packages/cli/README.md | 15 ++++++- packages/cli/oclif.manifest.json | 12 +++++- .../theme/src/cli/commands/theme/open.test.ts | 43 ++++++++++++++++++- packages/theme/src/cli/commands/theme/open.ts | 8 +++- 4 files changed, 72 insertions(+), 6 deletions(-) diff --git a/packages/cli/README.md b/packages/cli/README.md index 6cebbd9a30e..a976adae2b0 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -11937,6 +11937,11 @@ DESCRIPTION "message": { "type": "string" }, + "code": { + "type": "string", + "minLength": 1, + "description": "A stable error code, included only when known." + }, "tryMessage": { "type": "string" }, @@ -11977,7 +11982,9 @@ DESCRIPTION "additionalProperties": false } }, - "details": {} + "details": { + "description": "Selected domain details, preserving native API payloads such as GraphQL errors, extensions, and data." + } }, "required": [ "type", @@ -11995,6 +12002,9 @@ DESCRIPTION "message": { "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message" }, + "code": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code" + }, "tryMessage": { "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage" }, @@ -12027,6 +12037,9 @@ DESCRIPTION "message": { "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message" }, + "code": { + "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code" + }, "tryMessage": { "$ref": "#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage" }, diff --git a/packages/cli/oclif.manifest.json b/packages/cli/oclif.manifest.json index 2ed4e5fcd10..01474ba2a4e 100644 --- a/packages/cli/oclif.manifest.json +++ b/packages/cli/oclif.manifest.json @@ -11675,7 +11675,7 @@ "args": { }, "customPluginName": "@shopify/theme", - "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeOpenResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemeOpenResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include live, unpublished, and development.\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"$ref\": \"#/definitions/Theme/properties/id\"\n },\n \"name\": {\n \"$ref\": \"#/definitions/Theme/properties/name\"\n },\n \"role\": {\n \"$ref\": \"#/definitions/Theme/properties/role\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"previewUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"editorUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"previewUrl\",\n \"editorUrl\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {}\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", + "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\nUse `--json-schema` to print the result, error, and event schemas.\n\nOutput from `--json` conforms to the `ThemeOpenResult` schema.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/0/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemeOpenResult\",\n \"definitions\": {\n \"Theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"string\",\n \"pattern\": \"^\\\\d+$\",\n \"description\": \"The decimal Online Store theme ID, not a Shopify GID.\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\",\n \"description\": \"The upstream theme role; known values include live, unpublished, and development.\"\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeEnvironment\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"theme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"$ref\": \"#/definitions/Theme/properties/id\"\n },\n \"name\": {\n \"$ref\": \"#/definitions/Theme/properties/name\"\n },\n \"role\": {\n \"$ref\": \"#/definitions/Theme/properties/role\"\n },\n \"storeDomain\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"pattern\": \"^[a-z0-9][a-z0-9-]*\\\\.myshopify\\\\.com$\"\n },\n {\n \"type\": \"null\"\n }\n ]\n },\n \"previewUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"editorUrl\": {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n \"processing\": {\n \"type\": \"boolean\"\n },\n \"sourceUrl\": {\n \"anyOf\": [\n {\n \"type\": \"string\",\n \"format\": \"uri\"\n },\n {\n \"type\": \"null\"\n }\n ]\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"storeDomain\",\n \"previewUrl\",\n \"editorUrl\",\n \"processing\",\n \"sourceUrl\"\n ],\n \"additionalProperties\": false\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"status\": {\n \"type\": \"string\",\n \"const\": \"cancelled\"\n }\n },\n \"required\": [\n \"status\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"error\": {\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"abort\"\n },\n \"message\": {\n \"type\": \"string\"\n },\n \"code\": {\n \"type\": \"string\",\n \"minLength\": 1,\n \"description\": \"A stable error code, included only when known.\"\n },\n \"tryMessage\": {\n \"type\": \"string\"\n },\n \"nextSteps\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n },\n \"customSections\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"title\": {\n \"type\": \"string\"\n },\n \"body\": {\n \"anyOf\": [\n {\n \"type\": \"string\"\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n ]\n }\n },\n \"required\": [\n \"body\"\n ],\n \"additionalProperties\": false\n }\n },\n \"details\": {\n \"description\": \"Selected domain details, preserving native API payloads such as GraphQL errors, extensions, and data.\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"bug\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"code\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"stack\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"type\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"type\": {\n \"type\": \"string\",\n \"const\": \"external\"\n },\n \"message\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/message\"\n },\n \"code\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/code\"\n },\n \"tryMessage\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/tryMessage\"\n },\n \"nextSteps\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/nextSteps\"\n },\n \"customSections\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/customSections\"\n },\n \"details\": {\n \"$ref\": \"#/definitions/ThemeEnvironment/anyOf/1/properties/error/anyOf/0/properties/details\"\n },\n \"command\": {\n \"type\": \"string\"\n },\n \"args\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n },\n \"required\": [\n \"type\",\n \"message\",\n \"command\",\n \"args\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"required\": [\n \"environment\",\n \"error\"\n ],\n \"additionalProperties\": false\n }\n ]\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```", "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.", "enableJsonFlag": false, "flags": { @@ -11799,7 +11799,15 @@ "hiddenAliases": [ ], "id": "theme:open", - "multiEnvironmentsFlags": null, + "multiEnvironmentsFlags": [ + "store", + "password", + [ + "theme", + "live", + "development" + ] + ], "pluginAlias": "@shopify/cli", "pluginName": "@shopify/cli", "pluginType": "core", diff --git a/packages/theme/src/cli/commands/theme/open.test.ts b/packages/theme/src/cli/commands/theme/open.test.ts index 8dbd97c0351..8f42468cd2b 100644 --- a/packages/theme/src/cli/commands/theme/open.test.ts +++ b/packages/theme/src/cli/commands/theme/open.test.ts @@ -8,13 +8,14 @@ import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session' import {openURL} from '@shopify/cli-kit/node/system' import {renderInfo} from '@shopify/cli-kit/node/ui' import {withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output' +import {loadEnvironment} from '@shopify/cli-kit/node/environments' vi.mock('../../services/open.js') vi.mock('../../utilities/theme-store.js') vi.mock('@shopify/cli-kit/node/session') vi.mock('@shopify/cli-kit/node/system', async (importOriginal) => ({ ...(await importOriginal()), - openURL: vi.fn(), + openURL: vi.fn(async () => true), })) vi.mock('@shopify/cli-kit/node/ui') vi.mock('@shopify/cli-kit/node/environments') @@ -79,6 +80,36 @@ test('writes one JSON result', async () => { expect(open).toHaveBeenCalledWith(session, expect.objectContaining({theme: '1', json: true})) }) +test.each([false, true])('collects ordered environment results with browser failure=%s', async (browserFailure) => { + const previousExitCode = process.exitCode + try { + vi.mocked(loadEnvironment).mockResolvedValue({store: session.storeFqdn, password: 'token', theme: '1'}) + vi.mocked(open).mockResolvedValue(result) + vi.mocked(openURL).mockResolvedValueOnce(true).mockResolvedValueOnce(!browserFailure) + + await withCapturedStandardStreams(async ({stdout}) => { + await run(['--json', '-e', 'staging', '-e', 'production']) + + const output = JSON.parse(stdout()) + expect(output).toEqual({ + environments: [ + {environment: 'staging', result: publicResult}, + browserFailure + ? {environment: 'production', error: expect.objectContaining({message: 'Could not open the browser.'})} + : {environment: 'production', result: publicResult}, + ], + }) + expect(() => themeOpenJsonOutputSchema.validate(output)).not.toThrow() + }) + expect(openURL).toHaveBeenCalledTimes(2) + expect(open).toHaveBeenCalledTimes(2) + if (browserFailure) expect(process.exitCode).toBe(1) + } finally { + // eslint-disable-next-line require-atomic-updates + process.exitCode = previousExitCode + } +}) + test('propagates selection errors without opening the browser or rendering a result', async () => { const error = new Error('Theme not found') vi.mocked(open).mockRejectedValue(error) @@ -100,3 +131,13 @@ test('propagates browser failures before emitting the JSON result', async () => expect(stdout()).toBe('') }) }) + +test('rejects an unsuccessful browser launch without emitting a JSON result', async () => { + vi.mocked(open).mockResolvedValue(result) + vi.mocked(openURL).mockResolvedValue(false) + + await withCapturedStandardStreams(async ({stdout}) => { + await expect(run(['--theme=1', '--json'])).rejects.toThrow('Could not open the browser.') + expect(stdout()).toBe('') + }) +}) diff --git a/packages/theme/src/cli/commands/theme/open.ts b/packages/theme/src/cli/commands/theme/open.ts index a477ecdd0f4..262d6621cea 100644 --- a/packages/theme/src/cli/commands/theme/open.ts +++ b/packages/theme/src/cli/commands/theme/open.ts @@ -6,6 +6,7 @@ import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.js' import {Flags} from '@oclif/core' import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli' import {openURL} from '@shopify/cli-kit/node/system' +import {AbortError} from '@shopify/cli-kit/node/error' import {AdminSession} from '@shopify/cli-kit/node/session' import {InferredFlags} from '@oclif/core/interfaces' import type {NonTTYFlagRequirement} from '@shopify/cli-kit/node/base-command' @@ -56,7 +57,7 @@ export default class Open extends ThemeCommand { }), } - static multiEnvironmentsFlags: RequiredFlags = null + static multiEnvironmentsFlags: RequiredFlags = ['store', 'password', ['theme', 'live', 'development']] static nonTTYFlagRequirements(): NonTTYFlagRequirement[] { return [{flags: ['theme', 'development', 'live']}] @@ -65,7 +66,10 @@ export default class Open extends ThemeCommand { async command(flags: OpenFlags, adminSession: AdminSession, multiEnvironment = false) { const result = await open(adminSession, flags) if (!flags.json) renderThemeOpenResult(result, 'text') - await openURL(flags.editor ? result.editor_url : result.preview_url) + const url = flags.editor ? result.editor_url : result.preview_url + if (!(await openURL(url))) { + throw new AbortError('Could not open the browser.', `Open this URL manually: ${url}`) + } if (flags.json && !multiEnvironment) renderThemeOpenResult(result, 'json') return result }