Skip to content

Commit 0f5ba55

Browse files
authored
Merge pull request #6361 from Shopify/jb-multi-env-push-share
Allow theme push and share commands to be called with multiple environments
2 parents 7c68345 + 47d198d commit 0f5ba55

File tree

17 files changed

+421
-130
lines changed

17 files changed

+421
-130
lines changed

.changeset/clever-ducks-sleep.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@shopify/theme': minor
3+
'@shopify/cli': minor
4+
'@shopify/cli-kit': minor
5+
---
6+
7+
Allow theme push and share commands to be called with multiple environments

.changeset/light-ears-drum.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@shopify/theme': patch
3+
'@shopify/cli': patch
4+
---
5+
6+
Remove leftover references to CLI2 from theme commands

packages/cli-kit/src/private/node/ui/components/LoadingBar.test.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,4 +204,16 @@ describe('LoadingBar', () => {
204204
// Then
205205
expect(frame1()).toBe(frame2())
206206
})
207+
208+
test('hides progress bar when noProgressBar is true', async () => {
209+
// Given
210+
vi.mocked(shouldDisplayColors).mockReturnValue(true)
211+
const title = 'task 1'
212+
213+
// When
214+
const {lastFrame} = render(<LoadingBar title={title} noProgressBar />)
215+
216+
// Then
217+
expect(unstyled(lastFrame()!)).toMatchInlineSnapshot(`"task 1 ..."`)
218+
})
207219
})

packages/cli-kit/src/private/node/ui/components/LoadingBar.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,10 @@ const hillString = '▁▁▂▂▃▃▄▄▅▅▆▆▇▇██▇▇▆▆
1010
interface LoadingBarProps {
1111
title: string
1212
noColor?: boolean
13+
noProgressBar?: boolean
1314
}
1415

15-
const LoadingBar = ({title, noColor}: React.PropsWithChildren<LoadingBarProps>) => {
16+
const LoadingBar = ({title, noColor, noProgressBar}: React.PropsWithChildren<LoadingBarProps>) => {
1617
const {twoThirds} = useLayout()
1718
let loadingBar = new Array(twoThirds).fill(loadingBarChar).join('')
1819
if (noColor ?? !shouldDisplayColors()) {
@@ -21,7 +22,7 @@ const LoadingBar = ({title, noColor}: React.PropsWithChildren<LoadingBarProps>)
2122

2223
return (
2324
<Box flexDirection="column">
24-
<TextAnimation text={loadingBar} maxWidth={twoThirds} />
25+
{!noProgressBar && <TextAnimation text={loadingBar} maxWidth={twoThirds} />}
2526
<Text>{title} ...</Text>
2627
</Box>
2728
)

packages/cli-kit/src/private/node/ui/components/Tasks.tsx

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ interface TasksProps<TContext> {
2222
onComplete?: (ctx: TContext) => void
2323
abortSignal?: AbortSignal
2424
noColor?: boolean
25+
noProgressBar?: boolean
2526
}
2627

2728
enum TasksState {
@@ -63,6 +64,7 @@ function Tasks<TContext>({
6364
onComplete = noop,
6465
abortSignal,
6566
noColor,
67+
noProgressBar = false,
6668
}: React.PropsWithChildren<TasksProps<TContext>>) {
6769
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
6870
const [currentTask, setCurrentTask] = useState<Task<TContext>>(tasks[0]!)
@@ -105,7 +107,9 @@ function Tasks<TContext>({
105107
return null
106108
}
107109

108-
return state === TasksState.Loading && !isAborted ? <LoadingBar title={currentTask.title} noColor={noColor} /> : null
110+
return state === TasksState.Loading && !isAborted ? (
111+
<LoadingBar title={currentTask.title} noColor={noColor} noProgressBar={noProgressBar} />
112+
) : null
109113
}
110114

111115
export {Tasks}

packages/cli-kit/src/public/node/ui.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ export function renderTable<T extends ScalarDict>({renderOptions, ...props}: Ren
462462

463463
interface RenderTasksOptions {
464464
renderOptions?: RenderOptions
465+
noProgressBar?: boolean
465466
}
466467

467468
/**
@@ -471,10 +472,13 @@ interface RenderTasksOptions {
471472
* Installing dependencies ...
472473
*/
473474
// eslint-disable-next-line max-params
474-
export async function renderTasks<TContext>(tasks: Task<TContext>[], {renderOptions}: RenderTasksOptions = {}) {
475+
export async function renderTasks<TContext>(
476+
tasks: Task<TContext>[],
477+
{renderOptions, noProgressBar}: RenderTasksOptions = {},
478+
) {
475479
// eslint-disable-next-line max-params
476480
return new Promise<TContext>((resolve, reject) => {
477-
render(<Tasks tasks={tasks} onComplete={resolve} />, {
481+
render(<Tasks tasks={tasks} onComplete={resolve} noProgressBar={noProgressBar} />, {
478482
...renderOptions,
479483
exitOnCtrlC: false,
480484
})

packages/cli/oclif.manifest.json

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6674,6 +6674,16 @@
66746674
"hiddenAliases": [
66756675
],
66766676
"id": "theme:push",
6677+
"multiEnvironmentsFlags": [
6678+
"store",
6679+
"password",
6680+
"path",
6681+
[
6682+
"live",
6683+
"development",
6684+
"theme"
6685+
]
6686+
],
66776687
"pluginAlias": "@shopify/cli",
66786688
"pluginName": "@shopify/cli",
66796689
"pluginType": "core",
@@ -7001,9 +7011,6 @@
70017011
],
70027012
"args": {
70037013
},
7004-
"cli2Flags": [
7005-
"force"
7006-
],
70077014
"customPluginName": "@shopify/theme",
70087015
"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.",
70097016
"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.",
@@ -7073,6 +7080,11 @@
70737080
"hiddenAliases": [
70747081
],
70757082
"id": "theme:share",
7083+
"multiEnvironmentsFlags": [
7084+
"store",
7085+
"password",
7086+
"path"
7087+
],
70767088
"pluginAlias": "@shopify/cli",
70777089
"pluginName": "@shopify/cli",
70787090
"pluginType": "core",

packages/theme/src/cli/commands/theme/push.ts

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
import {globFlags, themeFlags} from '../../flags.js'
22
import ThemeCommand from '../../utilities/theme-command.js'
3-
import {push, PushFlags} from '../../services/push.js'
3+
import {push} from '../../services/push.js'
44
import {Flags} from '@oclif/core'
55
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
66
import {recordTiming} from '@shopify/cli-kit/node/analytics'
7+
import {AdminSession} from '@shopify/cli-kit/node/session'
8+
import {InferredFlags} from '@oclif/core/interfaces'
9+
import {Writable} from 'stream'
10+
11+
type PushFlags = InferredFlags<typeof Push.flags>
712

813
export default class Push extends ThemeCommand {
914
static summary = 'Uploads your local theme files to the connected store, overwriting the remote version if specified.'
@@ -91,33 +96,28 @@ export default class Push extends ThemeCommand {
9196
description: 'Require theme check to pass without errors before pushing. Warnings are allowed.',
9297
env: 'SHOPIFY_FLAG_STRICT_PUSH',
9398
}),
99+
environment: themeFlags.environment,
94100
}
95101

96-
async run(): Promise<void> {
97-
const {flags} = await this.parse(Push)
98-
99-
const pushFlags: PushFlags = {
100-
path: flags.path,
101-
password: flags.password,
102-
store: flags.store,
103-
theme: flags.theme,
104-
development: flags.development,
105-
live: flags.live,
106-
unpublished: flags.unpublished,
107-
nodelete: flags.nodelete,
108-
only: flags.only,
109-
ignore: flags.ignore,
110-
json: flags.json,
111-
allowLive: flags['allow-live'],
112-
publish: flags.publish,
113-
force: flags.force,
114-
noColor: flags['no-color'],
115-
verbose: flags.verbose,
116-
strict: flags.strict,
117-
}
102+
static multiEnvironmentsFlags = ['store', 'password', 'path', ['live', 'development', 'theme']]
118103

104+
async command(
105+
flags: PushFlags,
106+
adminSession: AdminSession,
107+
multiEnvironment: boolean,
108+
context?: {stdout?: Writable; stderr?: Writable},
109+
) {
119110
recordTiming('theme-command:push')
120-
await push(pushFlags)
111+
await push(
112+
{
113+
...flags,
114+
allowLive: flags['allow-live'],
115+
noColor: flags['no-color'],
116+
},
117+
adminSession,
118+
multiEnvironment,
119+
context,
120+
)
121121
recordTiming('theme-command:push')
122122
}
123123
}

packages/theme/src/cli/commands/theme/share.ts

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@ import {Flags} from '@oclif/core'
55
import {globalFlags} from '@shopify/cli-kit/node/cli'
66
import {getRandomName} from '@shopify/cli-kit/common/string'
77
import {recordTiming} from '@shopify/cli-kit/node/analytics'
8+
import {InferredFlags} from '@oclif/core/interfaces'
9+
import {AdminSession} from '@shopify/cli-kit/node/session'
10+
import {Writable} from 'stream'
811

12+
type ShareFlags = InferredFlags<typeof Share.flags>
913
export default class Share extends ThemeCommand {
1014
static summary = 'Creates a shareable, unpublished, and new theme on your theme library with a randomized name.'
1115

@@ -26,22 +30,27 @@ export default class Share extends ThemeCommand {
2630
}),
2731
}
2832

29-
static cli2Flags = ['force']
30-
31-
async run(): Promise<void> {
32-
const {flags} = await this.parse(Share)
33+
static multiEnvironmentsFlags = ['store', 'password', 'path']
3334

35+
async command(
36+
flags: ShareFlags,
37+
adminSession: AdminSession,
38+
multiEnvironment: boolean,
39+
context?: {stdout?: Writable; stderr?: Writable},
40+
) {
3441
const pushFlags: PushFlags = {
42+
environment: flags.environment,
3543
force: flags.force,
36-
path: flags.path,
44+
noColor: flags['no-color'],
3745
password: flags.password,
46+
path: flags.path,
3847
store: flags.store,
39-
unpublished: true,
4048
theme: getRandomName('creative'),
49+
unpublished: true,
4150
}
4251

4352
recordTiming('theme-command:share')
44-
await push(pushFlags)
53+
await push(pushFlags, adminSession, multiEnvironment, context)
4554
recordTiming('theme-command:share')
4655
}
4756
}

packages/theme/src/cli/services/push.test.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import {
1515
promptThemeName,
1616
UNPUBLISHED_THEME_ROLE,
1717
} from '@shopify/cli-kit/node/themes/utils'
18-
import {renderConfirmationPrompt} from '@shopify/cli-kit/node/ui'
18+
import {renderConfirmationPrompt, renderError} from '@shopify/cli-kit/node/ui'
1919
import {AbortError} from '@shopify/cli-kit/node/error'
2020
import {Severity, SourceCodeType} from '@shopify/theme-check-node'
2121
import {outputResult} from '@shopify/cli-kit/node/output'
@@ -45,7 +45,7 @@ const adminSession = {token: '', storeFqdn: ''}
4545

4646
describe('push', () => {
4747
beforeEach(() => {
48-
vi.mocked(uploadTheme).mockResolvedValue({
48+
vi.mocked(uploadTheme).mockReturnValue({
4949
workPromise: Promise.resolve(),
5050
uploadResults: new Map(),
5151
renderThemeSyncProgress: () => Promise.resolve(),
@@ -61,7 +61,7 @@ describe('push', () => {
6161
vi.mocked(findOrSelectTheme).mockResolvedValue(theme)
6262

6363
// When
64-
await push({...defaultFlags, publish: true})
64+
await push({...defaultFlags, publish: true}, adminSession)
6565

6666
// Then
6767
expect(themePublish).toHaveBeenCalledWith(theme.id, adminSession)
@@ -89,14 +89,14 @@ describe('push', () => {
8989
success: true,
9090
})
9191

92-
vi.mocked(uploadTheme).mockResolvedValue({
92+
vi.mocked(uploadTheme).mockReturnValue({
9393
workPromise: Promise.resolve(),
9494
uploadResults,
9595
renderThemeSyncProgress: () => Promise.resolve(),
9696
})
9797

9898
// When
99-
await push({...defaultFlags, json: true})
99+
await push({...defaultFlags, json: true}, adminSession)
100100

101101
// Then
102102
expect(outputResult).toHaveBeenCalledWith(
@@ -129,7 +129,7 @@ describe('push', () => {
129129
const flags = {...defaultFlags, strict: false}
130130

131131
// When
132-
await push(flags)
132+
await push(flags, adminSession)
133133

134134
// Then
135135
expect(runThemeCheck).not.toHaveBeenCalled()
@@ -153,7 +153,7 @@ describe('push', () => {
153153
})
154154

155155
// When/Then
156-
await expect(push({...defaultFlags, strict: true})).rejects.toThrow(AbortError)
156+
await expect(push({...defaultFlags, strict: true}, adminSession)).rejects.toThrow(AbortError)
157157
})
158158

159159
test('blocks push when both warnings and errors exist', async () => {
@@ -183,7 +183,7 @@ describe('push', () => {
183183
})
184184

185185
// When/Then
186-
await expect(push({...defaultFlags, strict: true})).rejects.toThrow(AbortError)
186+
await expect(push({...defaultFlags, strict: true}, adminSession)).rejects.toThrow(AbortError)
187187
})
188188

189189
test('continues push when no offenses exist', async () => {
@@ -194,7 +194,7 @@ describe('push', () => {
194194
})
195195

196196
// When/Then
197-
await expect(push({...defaultFlags, strict: true})).resolves.not.toThrow()
197+
await expect(push({...defaultFlags, strict: true}, adminSession)).resolves.not.toThrow()
198198
})
199199

200200
test('continues push when only warnings exist', async () => {
@@ -215,7 +215,7 @@ describe('push', () => {
215215
})
216216

217217
// When/Then
218-
await expect(push({...defaultFlags, strict: true})).resolves.not.toThrow()
218+
await expect(push({...defaultFlags, strict: true}, adminSession)).resolves.not.toThrow()
219219
})
220220

221221
test('passes the --json flag to theme check as output format', async () => {
@@ -236,7 +236,7 @@ describe('push', () => {
236236
})
237237

238238
// When/Then
239-
await push({...defaultFlags, strict: true, json: true})
239+
await push({...defaultFlags, strict: true, json: true}, adminSession)
240240
expect(runThemeCheck).toHaveBeenCalledWith(path, 'json')
241241
})
242242
})
@@ -361,4 +361,25 @@ describe('createOrSelectTheme', async () => {
361361
// Then
362362
expect(promptThemeName).toHaveBeenCalledWith('Name of the new theme')
363363
})
364+
365+
describe('when run during a multi environment command', () => {
366+
test('displays error when live theme is selected without allow-live flag', async () => {
367+
// Given
368+
vi.mocked(findOrSelectTheme).mockResolvedValue(buildTheme({id: 3, name: 'Live Theme', role: LIVE_THEME_ROLE})!)
369+
const flags: PushFlags = {live: true, environment: ['production']}
370+
371+
// When
372+
const theme = await createOrSelectTheme(adminSession, flags, true)
373+
374+
// Then
375+
expect(theme).toBeUndefined()
376+
expect(renderError).toHaveBeenCalledWith({
377+
headline: 'Environment: production',
378+
body: [
379+
`Can't push theme files to the live theme on ${adminSession.storeFqdn}`,
380+
'Use the --allow-live flag to push to a live theme.',
381+
],
382+
})
383+
})
384+
})
364385
})

0 commit comments

Comments
 (0)