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 @@ -9174,6 +9174,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-share.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-share.interface.ts",
"syntaxKind": "PropertySignature",
Expand All @@ -9184,7 +9193,7 @@
"environmentValue": "SHOPIFY_FLAG_STORE"
}
],
"value": "export interface themeshare {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.\n * @environment SHOPIFY_FLAG_LISTING\n */\n '--listing <value>'?: string\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 * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
"value": "export interface themeshare {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Output the 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 * The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.\n * @environment SHOPIFY_FLAG_LISTING\n */\n '--listing <value>'?: string\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 * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
}
},
"upgrade": {
Expand Down
101 changes: 99 additions & 2 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9649,14 +9649,18 @@ Creates a shareable, unpublished, and new theme on your theme library with a ran

```
USAGE
$ shopify theme share [--auth-alias <value>] [-e <value>...] [--json-schema] [--listing <value>] [--no-color]
[--password <value>] [--path <value>] [-s <value>] [--verbose]
$ shopify theme share [--auth-alias <value>] [-e <value>...] [-j] [--json-schema] [--listing <value>]
[--no-color] [--password <value>] [--path <value>] [-s <value>] [--verbose]

FLAGS
-e, --environment=<value>...
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]

-s, --store=<value>
Store URL. It can be the store prefix (example) or the full myshopify.com URL (example.myshopify.com,
https://example.myshopify.com).
Expand Down Expand Up @@ -9698,6 +9702,99 @@ DESCRIPTION
This command returns a "preview link"
(https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can
share with others.

Output from `--json` conforms to the `ThemeShareResult` schema.

Use `--json-schema` to print the result, error, and event schemas.

```json
{
"anyOf": [
{
"type": "object",
"properties": {
"environment": {
"type": "string"
},
"theme": {
"$ref": "#/definitions/ThemePushTheme"
}
},
"required": [
"theme"
],
"additionalProperties": false
},
{
"type": "array",
"items": {
"type": "object",
"properties": {
"environment": {
"type": "string"
},
"theme": {
"$ref": "#/definitions/ThemePushTheme"
}
},
"required": [
"environment",
"theme"
],
"additionalProperties": false
}
}
],
"title": "ThemeShareResult",
"definitions": {
"ThemePushTheme": {
"type": "object",
"properties": {
"id": {
"type": "number"
},
"name": {
"type": "string"
},
"role": {
"type": "string"
},
"shop": {
"type": "string"
},
"editor_url": {
"type": "string"
},
"preview_url": {
"type": "string"
},
"warning": {
"type": "string"
},
"errors": {
"type": "object",
"additionalProperties": {
"type": "array",
"items": {
"type": "string"
}
}
}
},
"required": [
"id",
"name",
"role",
"shop",
"editor_url",
"preview_url"
],
"additionalProperties": false
}
},
"$schema": "http://json-schema.org/draft-07/schema#"
}
```
```

## `shopify upgrade`
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 @@ -11696,7 +11696,7 @@
"args": {
},
"customPluginName": "@shopify/theme",
"description": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.",
"description": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a \"preview link\" (https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.\n\nOutput from `--json` conforms to the `ThemeShareResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"anyOf\": [\n {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePushTheme\"\n }\n },\n \"required\": [\n \"theme\"\n ],\n \"additionalProperties\": false\n },\n {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"theme\": {\n \"$ref\": \"#/definitions/ThemePushTheme\"\n }\n },\n \"required\": [\n \"environment\",\n \"theme\"\n ],\n \"additionalProperties\": false\n }\n }\n ],\n \"title\": \"ThemeShareResult\",\n \"definitions\": {\n \"ThemePushTheme\": {\n \"type\": \"object\",\n \"properties\": {\n \"id\": {\n \"type\": \"number\"\n },\n \"name\": {\n \"type\": \"string\"\n },\n \"role\": {\n \"type\": \"string\"\n },\n \"shop\": {\n \"type\": \"string\"\n },\n \"editor_url\": {\n \"type\": \"string\"\n },\n \"preview_url\": {\n \"type\": \"string\"\n },\n \"warning\": {\n \"type\": \"string\"\n },\n \"errors\": {\n \"type\": \"object\",\n \"additionalProperties\": {\n \"type\": \"array\",\n \"items\": {\n \"type\": \"string\"\n }\n }\n }\n },\n \"required\": [\n \"id\",\n \"name\",\n \"role\",\n \"shop\",\n \"editor_url\",\n \"preview_url\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
"descriptionWithMarkdown": "Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.\n\n This command returns a [preview link](https://help.shopify.com/manual/online-store/themes/adding-themes#share-a-theme-preview-with-others) that you can share with others.",
"enableJsonFlag": false,
"flags": {
Expand Down Expand Up @@ -11726,6 +11726,15 @@
"name": "force",
"type": "boolean"
},
"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 @@ -66,7 +66,6 @@ const commandExceptions = [
'packages/theme/src/cli/commands/theme/package.ts',
'packages/theme/src/cli/commands/theme/preview.ts',
'packages/theme/src/cli/commands/theme/profile.ts',
'packages/theme/src/cli/commands/theme/share.ts',

// Streaming commands without a single finite result.
'packages/app/src/cli/commands/app/demo/watcher.ts',
Expand Down
116 changes: 116 additions & 0 deletions packages/theme/src/cli/commands/theme/share.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import Share from './share.js'
import {executeThemePush} from '../../services/push.js'
import {themeShareJsonOutputSchema} from '../../services/share/types.js'
import {Config} from '@oclif/core'
import {getRandomName} from '@shopify/cli-kit/common/string'
import {mockAndCaptureOutput, withCapturedStandardStreams} from '@shopify/cli-kit/node/testing/output'
import {describe, expect, test, vi} from 'vitest'

vi.mock('../../services/push.js')
vi.mock('@shopify/cli-kit/common/string', async (importOriginal) => ({
...(await importOriginal<typeof import('@shopify/cli-kit/common/string')>()),
getRandomName: vi.fn(),
}))

const session = {storeFqdn: 'test.myshopify.com', token: 'token'}

function result() {
return {
theme: {
id: 1,
name: 'Creative Theme',
role: 'unpublished',
shop: session.storeFqdn,
editor_url: 'editor',
preview_url: 'preview',
},
published: false,
hasErrors: false,
errors: {},
}
}

describe('theme share', () => {
test('exposes JSON and schema flags and documents its own result', () => {
expect(Share.jsonOutputSchema).toBe(themeShareJsonOutputSchema)
expect(Share.description).toContain('ThemeShareResult')
expect(Share.flags.json).toBeDefined()
expect(Share.baseFlags).toHaveProperty('json-schema')
})

test('creates an unpublished theme with a random name and forwards the listing', async () => {
vi.mocked(getRandomName).mockReturnValue('Creative Theme')
vi.mocked(executeThemePush).mockResolvedValue(result())
const command = new Share([], new Config({root: '.'}))
await withCapturedStandardStreams(async ({stdout, stderr}) => {
await command.command({json: true, path: '/theme', listing: 'preset'} as never, session, false)

expect(getRandomName).toHaveBeenCalledWith('creative')
expect(executeThemePush).toHaveBeenCalledWith(
expect.objectContaining({unpublished: true, theme: 'Creative Theme', path: '/theme', listing: 'preset'}),
session,
false,
undefined,
)
expect(stdout()).toBe(`${themeShareJsonOutputSchema.encode({theme: result().theme})}\n`)
expect(stderr()).toBe('')
})
})

test('keeps upload errors and environment in JSON', async () => {
vi.mocked(executeThemePush).mockResolvedValue({
...result(),
environment: 'staging',
hasErrors: true,
errors: {'assets/theme.css': ['bad CSS']},
})
const command = new Share([], new Config({root: '.'}))
await withCapturedStandardStreams(async ({stdout}) => {
await command.command({json: true, environment: ['staging']} as never, session, false)

expect(JSON.parse(stdout())).toEqual({
environment: 'staging',
theme: {
...result().theme,
warning: "[staging] The theme 'Creative Theme' was pushed with errors",
errors: {'assets/theme.css': ['bad CSS']},
},
})
})
})

test('preserves the push success banner in text mode', async () => {
vi.mocked(executeThemePush).mockResolvedValue(result())
const output = mockAndCaptureOutput()
output.clear()
await new Share([], new Config({root: '.'})).command({} as never, session, false)
expect(output.info()).toContain("The theme 'Creative Theme' (#1) was pushed successfully.")
expect(output.info()).toContain('View your theme')
expect(output.info()).toContain('Customize your theme at the theme editor')
})

test('does not emit a result when cancelled', async () => {
vi.mocked(executeThemePush).mockResolvedValue(undefined)
await withCapturedStandardStreams(async ({stdout}) => {
await new Share([], new Config({root: '.'})).command({json: true} as never, session, false)

expect(stdout()).toBe('')
})
})

test('propagates theme creation failures through the shared error path', async () => {
const failure = new Error('theme creation failed')
vi.mocked(executeThemePush).mockRejectedValue(failure)
await withCapturedStandardStreams(async ({stdout}) => {
await expect(new Share([], new Config({root: '.'})).command({json: true} as never, session, false)).rejects.toBe(
failure,
)

expect(stdout()).toBe('')
})
})

test.each([{id: '1'}, {role: null}, {errors: {file: 'invalid'}}])('rejects malformed theme data %j', (fields) => {
expect(() => themeShareJsonOutputSchema.validate({theme: {...result().theme, ...fields}})).toThrow()
})
})
23 changes: 20 additions & 3 deletions packages/theme/src/cli/commands/theme/share.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {themeShareJsonOutputSchema} from '../../services/share/types.js'
import {renderThemeShareResult, renderThemeShareEnvironmentResults} from '../../services/share/result.js'
import {themeFlags} from '../../flags.js'
import ThemeCommand from '../../utilities/theme-command.js'
import {push, PushFlags} from '../../services/push.js'
import {executeThemePush, PushFlags} from '../../services/push.js'
import {Flags} from '@oclif/core'
import {globalFlags} from '@shopify/cli-kit/node/cli'
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
import {getRandomName} from '@shopify/cli-kit/common/string'
import {recordTiming} from '@shopify/cli-kit/node/analytics'
import {InferredFlags} from '@oclif/core/interfaces'
Expand All @@ -13,6 +15,10 @@ import {Writable} from 'stream'

type ShareFlags = InferredFlags<typeof Share.flags>
export default class Share extends ThemeCommand {
static get jsonOutputSchema() {
return themeShareJsonOutputSchema
}

static summary = 'Creates a shareable, unpublished, and new theme on your theme library with a randomized name.'

static descriptionWithMarkdown = `Uploads your theme as a new, unpublished theme in your theme library. The theme is given a randomized name.
Expand All @@ -23,6 +29,7 @@ export default class Share extends ThemeCommand {

static flags = {
...globalFlags,
...jsonFlag,
...themeFlags,
force: Flags.boolean({
hidden: true,
Expand Down Expand Up @@ -59,7 +66,17 @@ export default class Share extends ThemeCommand {
}

recordTiming('theme-command:share')
await push(pushFlags, adminSession, multiEnvironment, context)
const result = await executeThemePush(pushFlags, adminSession, multiEnvironment, context)
if (result && !(flags.json && multiEnvironment)) renderThemeShareResult(result, flags.json ? 'json' : 'text')
recordTiming('theme-command:share')
return result
}

protected collectsEnvironmentResults(flags: {json?: boolean}): boolean {
return Boolean(flags.json)
}

protected renderEnvironmentResults(results: {environment: string; result: unknown}[]): void {
renderThemeShareEnvironmentResults(results)
}
}
Loading
Loading