Skip to content
7 changes: 7 additions & 0 deletions .changeset/polite-eyes-warn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@shopify/cli-kit': patch
'@shopify/theme': patch
'@shopify/cli': patch
---

Add --listing flag to theme dev, push, and share commands
6 changes: 6 additions & 0 deletions docs-shopify.dev/commands/interfaces/theme-dev.interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ export interface themedev {
*/
'-x, --ignore <value>'?: string

/**
* The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.
* @environment SHOPIFY_FLAG_LISTING
*/
'--listing <value>'?: string

/**
* The live reload mode switches the server behavior when a file is modified:
- hot-reload Hot reloads local changes to CSS and sections (default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export interface themepush {
*/
'-j, --json'?: ''

/**
* The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.
* @environment SHOPIFY_FLAG_LISTING
*/
'--listing <value>'?: string

/**
* Push theme files from your remote live theme.
* @environment SHOPIFY_FLAG_LIVE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ export interface themeshare {
*/
'-e, --environment <value>'?: string

/**
* The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.
* @environment SHOPIFY_FLAG_LISTING
*/
'--listing <value>'?: string

/**
* Disable color output.
* @environment SHOPIFY_FLAG_NO_COLOR
Expand Down
33 changes: 30 additions & 3 deletions docs-shopify.dev/generated/generated_docs_data.json

Large diffs are not rendered by default.

12 changes: 11 additions & 1 deletion packages/cli-kit/src/public/common/string.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {takeRandomFromArray} from './array.js'
import {unstyled} from '../../public/node/output.js'
import {Token, TokenItem} from '../../private/node/ui/components/TokenizedText.js'
import {camelCase, constantCase, paramCase, snakeCase, pascalCase} from 'change-case'
import {camelCase, capitalCase, constantCase, paramCase, snakeCase, pascalCase} from 'change-case'

const SAFE_RANDOM_BUSINESS_ADJECTIVES = [
'commercial',
Expand Down Expand Up @@ -307,6 +307,16 @@ export function camelize(input: string): string {
return camelCase(input)
}

/**
* Transform a string to capitalCase.
*
* @param input - String to transform.
* @returns The transformed string.
*/
export function capitalizeWords(input: string): string {
return capitalCase(input)
}

/**
* Transform a string to param-case.
*
Expand Down
3 changes: 2 additions & 1 deletion packages/cli-kit/src/public/node/themes/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@ export type ThemeFSEventPayload<T extends ThemeFSEventName = 'add'> = (ThemeFSEv

export interface ThemeFileSystemOptions {
filters?: {ignore?: string[]; only?: string[]}
notify?: string
listing?: string
noDelete?: boolean
notify?: string
}

/**
Expand Down
15 changes: 11 additions & 4 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1908,8 +1908,8 @@ Uploads the current theme as a development theme to the connected store, then pr
```
USAGE
$ shopify theme dev [-a] [-e <value>...] [--error-overlay silent|default] [--host <value>] [-x <value>...]
[--live-reload hot-reload|full-page|off] [--no-color] [-n] [--notify <value>] [-o <value>...] [--open] [--password
<value>] [--path <value>] [--port <value>] [-s <value>] [--store-password <value>] [-t <value>]
[--listing <value>] [--live-reload hot-reload|full-page|off] [--no-color] [-n] [--notify <value>] [-o <value>...]
[--open] [--password <value>] [--path <value>] [--port <value>] [-s <value>] [--store-password <value>] [-t <value>]
[--theme-editor-sync] [--verbose]

FLAGS
Expand Down Expand Up @@ -1946,6 +1946,9 @@ FLAGS
--host=<value>
Set which network interface the web server listens on. The default value is 127.0.0.1.

--listing=<value>
The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.

--live-reload=<option>
[default: hot-reload] The live reload mode switches the server behavior when a file is modified:
- hot-reload Hot reloads local changes to CSS and sections (default)
Expand Down Expand Up @@ -2395,6 +2398,8 @@ FLAGS
-u, --unpublished Create a new unpublished theme and push to it.
-x, --ignore=<value>... Skip uploading the specified files (Multiple flags allowed). Wrap the value in double
quotes if you're using wildcards.
--listing=<value> The listing preset to use for multi-preset themes. Applies preset files from
listings/[preset-name] directory.
--no-color Disable color output.
--password=<value> Password generated from the Theme Access app or an Admin API token.
--path=<value> The path where you want to run the command. Defaults to the current working directory.
Expand Down Expand Up @@ -2475,13 +2480,15 @@ Creates a shareable, unpublished, and new theme on your theme library with a ran

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

FLAGS
-e, --environment=<value>... The environment to apply to the current command.
-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).
--listing=<value> The listing preset to use for multi-preset themes. Applies preset files from
listings/[preset-name] directory.
--no-color Disable color output.
--password=<value> Password generated from the Theme Access app or an Admin API token.
--path=<value> The path where you want to run the command. Defaults to the current working directory.
Expand Down
32 changes: 32 additions & 0 deletions packages/cli/oclif.manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5913,6 +5913,14 @@
"name": "ignore",
"type": "option"
},
"listing": {
"description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.",
"env": "SHOPIFY_FLAG_LISTING",
"hasDynamicHelp": false,
"multiple": false,
"name": "listing",
"type": "option"
},
"live-reload": {
"default": "hot-reload",
"description": "The live reload mode switches the server behavior when a file is modified:\n- hot-reload Hot reloads local changes to CSS and sections (default)\n- full-page Always refreshes the entire page\n- off Deactivate live reload",
Expand Down Expand Up @@ -7106,6 +7114,14 @@
"name": "json",
"type": "boolean"
},
"listing": {
"description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.",
"env": "SHOPIFY_FLAG_LISTING",
"hasDynamicHelp": false,
"multiple": false,
"name": "listing",
"type": "option"
},
"live": {
"allowNo": false,
"char": "l",
Expand Down Expand Up @@ -7411,6 +7427,14 @@
"name": "ignore",
"type": "option"
},
"listing": {
"description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.",
"env": "SHOPIFY_FLAG_LISTING",
"hasDynamicHelp": false,
"multiple": false,
"name": "listing",
"type": "option"
},
"live-reload": {
"default": "hot-reload",
"description": "The live reload mode switches the server behavior when a file is modified:\n- hot-reload Hot reloads local changes to CSS and sections (default)\n- full-page Always refreshes the entire page\n- off Deactivate live reload",
Expand Down Expand Up @@ -7578,6 +7602,14 @@
"name": "force",
"type": "boolean"
},
"listing": {
"description": "The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.",
"env": "SHOPIFY_FLAG_LISTING",
"hasDynamicHelp": false,
"multiple": false,
"name": "listing",
"type": "option"
},
"no-color": {
"allowNo": false,
"description": "Disable color output.",
Expand Down
6 changes: 6 additions & 0 deletions packages/theme/src/cli/commands/theme/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,11 @@ You can run this command only in a directory that matches the [default Shopify t
description: 'Theme ID or name of the remote theme.',
env: 'SHOPIFY_FLAG_THEME_ID',
}),
listing: Flags.string({
description:
'The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.',
env: 'SHOPIFY_FLAG_LISTING',
}),
nodelete: Flags.boolean({
char: 'n',
description:
Expand Down Expand Up @@ -165,6 +170,7 @@ You can run this command only in a directory that matches the [default Shopify t
password: flags.password,
storePassword: flags['store-password'],
theme,
listing: flags.listing,
host: flags.host,
port: flags.port,
'live-reload': flags['live-reload'] as LiveReload,
Expand Down
5 changes: 5 additions & 0 deletions packages/theme/src/cli/commands/theme/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,11 @@ export default class Push extends ThemeCommand {
description: 'Require theme check to pass without errors before pushing. Warnings are allowed.',
env: 'SHOPIFY_FLAG_STRICT_PUSH',
}),
listing: Flags.string({
description:
'The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.',
env: 'SHOPIFY_FLAG_LISTING',
}),
environment: themeFlags.environment,
}

Expand Down
6 changes: 6 additions & 0 deletions packages/theme/src/cli/commands/theme/share.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ export default class Share extends ThemeCommand {
description: 'Proceed without confirmation, if current directory does not seem to be theme directory.',
env: 'SHOPIFY_FLAG_FORCE',
}),
listing: Flags.string({
description:
'The listing preset to use for multi-preset themes. Applies preset files from listings/[preset-name] directory.',
env: 'SHOPIFY_FLAG_LISTING',
}),
}

static multiEnvironmentsFlags = ['store', 'password', 'path']
Expand All @@ -49,6 +54,7 @@ export default class Share extends ThemeCommand {
store: flags.store,
theme: getRandomName('creative'),
unpublished: true,
listing: flags.listing,
}

recordTiming('theme-command:share')
Expand Down
9 changes: 8 additions & 1 deletion packages/theme/src/cli/services/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {isStorefrontPasswordProtected} from '../utilities/theme-environment/stor
import {ensureValidPassword} from '../utilities/theme-environment/storefront-password-prompt.js'
import {emptyThemeExtFileSystem} from '../utilities/theme-fs-empty.js'
import {initializeDevServerSession} from '../utilities/theme-environment/dev-server-session.js'
import {ensureListingExists} from '../utilities/theme-listing.js'
import {renderSuccess, renderWarning} from '@shopify/cli-kit/node/ui'
import {AdminSession} from '@shopify/cli-kit/node/session'
import {Theme} from '@shopify/cli-kit/node/themes/types'
Expand Down Expand Up @@ -37,6 +38,7 @@ interface DevOptions {
ignore: string[]
only: string[]
notify?: string
listing?: string
}

export async function dev(options: DevOptions) {
Expand Down Expand Up @@ -66,15 +68,20 @@ export async function dev(options: DevOptions) {
})
}

if (options.listing) {
await ensureListingExists(options.directory, options.listing)
}

const storefrontPasswordPromise = await isStorefrontPasswordProtected(options.adminSession).then((needsPassword) =>
needsPassword ? ensureValidPassword(options.storePassword, options.adminSession.storeFqdn) : undefined,
)

const localThemeExtensionFileSystem = emptyThemeExtFileSystem()
const localThemeFileSystem = mountThemeFileSystem(options.directory, {
filters: options,
notify: options.notify,
listing: options.listing,
noDelete: options.noDelete,
notify: options.notify,
})

const host = options.host ?? DEFAULT_HOST
Expand Down
22 changes: 21 additions & 1 deletion packages/theme/src/cli/services/push.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,16 @@ import {uploadTheme} from '../utilities/theme-uploader.js'
import {ensureThemeStore} from '../utilities/theme-store.js'
import {findOrSelectTheme} from '../utilities/theme-selector.js'
import {runThemeCheck} from '../commands/theme/check.js'
import {mountThemeFileSystem} from '../utilities/theme-fs.js'
import {buildTheme} from '@shopify/cli-kit/node/themes/factories'
import {test, describe, vi, expect, beforeEach} from 'vitest'
import {themeCreate, fetchTheme, themePublish} from '@shopify/cli-kit/node/themes/api'
import {ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
import {
DEVELOPMENT_THEME_ROLE,
LIVE_THEME_ROLE,
promptThemeName,
UNPUBLISHED_THEME_ROLE,
promptThemeName,
} from '@shopify/cli-kit/node/themes/utils'
import {renderConfirmationPrompt, renderError} from '@shopify/cli-kit/node/ui'
import {AbortError} from '@shopify/cli-kit/node/error'
Expand All @@ -24,12 +25,14 @@ vi.mock('../utilities/theme-uploader.js')
vi.mock('../utilities/theme-store.js')
vi.mock('../utilities/theme-selector.js')
vi.mock('./local-storage.js')
vi.mock('../utilities/theme-listing.js')
vi.mock('@shopify/cli-kit/node/themes/utils')
vi.mock('@shopify/cli-kit/node/session')
vi.mock('@shopify/cli-kit/node/themes/api')
vi.mock('@shopify/cli-kit/node/ui')
vi.mock('../commands/theme/check.js')
vi.mock('@shopify/cli-kit/node/output')
vi.mock('../utilities/theme-fs.js')

const path = '/my-theme'
const defaultFlags: PullFlags = {
Expand Down Expand Up @@ -240,6 +243,23 @@ describe('push', () => {
expect(runThemeCheck).toHaveBeenCalledWith(path, 'json')
})
})

test('passes listing flag to file system when provided', async () => {
// Given
const theme = buildTheme({id: 1, name: 'Theme', role: 'development'})!
vi.mocked(findOrSelectTheme).mockResolvedValue(theme)

// When
await push({...defaultFlags, listing: 'my-preset'})

// Then
expect(mountThemeFileSystem).toHaveBeenCalledWith(
path,
expect.objectContaining({
listing: 'my-preset',
}),
)
})
})

describe('createOrSelectTheme', async () => {
Expand Down
15 changes: 14 additions & 1 deletion packages/theme/src/cli/services/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {Role} from '../utilities/theme-selector/fetch.js'
import {configureCLIEnvironment} from '../utilities/cli-config.js'
import {runThemeCheck} from '../commands/theme/check.js'
import {ensureThemeStore} from '../utilities/theme-store.js'
import {ensureListingExists} from '../utilities/theme-listing.js'
import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
import {themeCreate, fetchChecksums, themePublish} from '@shopify/cli-kit/node/themes/api'
import {Result, Theme} from '@shopify/cli-kit/node/themes/types'
Expand Down Expand Up @@ -38,6 +39,7 @@ interface PushOptions {
allowLive?: boolean
environment?: string
multiEnvironment?: boolean
listing?: string
}

interface JsonOutput {
Expand Down Expand Up @@ -108,6 +110,9 @@ export interface PushFlags {

/** The environment to push the theme to. */
environment?: string[]

/** The listing preset to use for multi-preset themes. */
listing?: string
}

/**
Expand Down Expand Up @@ -161,6 +166,10 @@ export async function push(
return
}

if (flags.listing) {
await ensureListingExists(workingDirectory, flags.listing)
}

const selectedTheme: Theme | undefined = await createOrSelectTheme(session, flags, multiEnvironment)
if (!selectedTheme) {
return
Expand All @@ -182,6 +191,7 @@ export async function push(
only: flags.only ?? [],
path: workingDirectory,
publish: flags.publish ?? false,
listing: flags.listing,
},
context,
)
Expand All @@ -202,7 +212,10 @@ async function executePush(
) {
recordTiming('theme-service:push:file-system')
const themeChecksums = await fetchChecksums(theme.id, session)
const themeFileSystem = mountThemeFileSystem(options.path, {filters: options})
const themeFileSystem = mountThemeFileSystem(options.path, {
filters: options,
listing: options.listing,
})
recordTiming('theme-service:push:file-system')

const {uploadResults, renderThemeSyncProgress} = uploadTheme(
Expand Down
Loading
Loading