Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 10 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -8170,6 +8170,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",
Expand Down Expand Up @@ -8198,7 +8207,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 <value>'?: string\n\n /**\n * Open your development theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\n\n /**\n * Open the theme editor for the specified theme in the browser.\n * @environment SHOPIFY_FLAG_EDITOR\n */\n '-E, --editor'?: ''\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Open your live (published) theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * Theme ID or name of the remote theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
"value": "export interface themeopen {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * Open your development theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_DEVELOPMENT\n */\n '-d, --development'?: ''\n\n /**\n * Open the theme editor for the specified theme in the browser.\n * @environment SHOPIFY_FLAG_EDITOR\n */\n '-E, --editor'?: ''\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * Open your live (published) theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_LIVE\n */\n '-l, --live'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Password generated from the Theme Access app or an Admin API token.\n * @environment SHOPIFY_CLI_THEME_TOKEN\n */\n '--password <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com, https://example.myshopify.com).\n * @environment SHOPIFY_FLAG_STORE\n */\n '-s, --store <value>'?: string\n\n /**\n * Theme ID or name of the remote theme. Use --development, --live, or --theme in non-interactive environments.\n * @environment SHOPIFY_FLAG_THEME_ID\n */\n '-t, --theme <value>'?: string\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"themepackage": {
Expand Down
68 changes: 67 additions & 1 deletion packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8412,7 +8412,7 @@ Opens the preview of your remote theme.

```
USAGE
$ shopify theme open [--auth-alias <value>] [-d] [-E] [-e <value>...] [--json-schema] [-l] [--no-color]
$ shopify theme open [--auth-alias <value>] [-d] [-E] [-e <value>...] [-j] [--json-schema] [-l] [--no-color]
[--password <value>] [--path <value>] [-s <value>] [-t <value>] [--verbose]

FLAGS
Expand All @@ -8428,6 +8428,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]
Expand Down Expand Up @@ -8477,6 +8481,68 @@ 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
{
"type": "object",
"properties": {
"theme": {
"$ref": "#/definitions/Theme"
},
"preview_url": {
"type": "string"
},
"editor_url": {
"type": "string"
}
},
"required": [
"theme",
"preview_url",
"editor_url"
],
"additionalProperties": false,
"title": "ThemeOpenResult",
"definitions": {
"Theme": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"createdAtRuntime": {
"type": "boolean"
},
"processing": {
"type": "boolean"
},
"role": {
"type": "string"
},
"src": {
"type": "string"
}
},
"required": [
"id",
"name",
"createdAtRuntime",
"processing",
"role"
],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
```
```

## `shopify theme package`
Expand Down
11 changes: 10 additions & 1 deletion packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10604,7 +10604,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": {
Expand Down Expand Up @@ -10641,6 +10641,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.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,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',
Expand Down
89 changes: 89 additions & 0 deletions packages/theme/src/cli/commands/theme/open.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import('@shopify/cli-kit/node/system')>()),
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)
})
})
14 changes: 12 additions & 2 deletions packages/theme/src/cli/commands/theme/open.ts
Original file line number Diff line number Diff line change
@@ -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<typeof Open.flags>
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:
Expand All @@ -22,6 +29,7 @@ export default class Open extends ThemeCommand {

static flags = {
...globalFlags,
...jsonFlag,
...themeFlags,
development: Flags.boolean({
char: 'd',
Expand Down Expand Up @@ -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)
}
}
66 changes: 14 additions & 52 deletions packages/theme/src/cli/services/open.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading
Loading