Skip to content

Commit cd188b4

Browse files
authored
Merge pull request #6390 from Shopify/jb-multi-env-pull
Allow theme pull command to be called with multiple environments
2 parents 71f0d4f + f8df96b commit cd188b4

File tree

6 files changed

+97
-41
lines changed

6 files changed

+97
-41
lines changed

.changeset/tricky-bugs-float.md

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

packages/cli/oclif.manifest.json

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6503,6 +6503,16 @@
65036503
"hiddenAliases": [
65046504
],
65056505
"id": "theme:pull",
6506+
"multiEnvironmentsFlags": [
6507+
"store",
6508+
"password",
6509+
"path",
6510+
[
6511+
"live",
6512+
"development",
6513+
"theme"
6514+
]
6515+
],
65066516
"pluginAlias": "@shopify/cli",
65076517
"pluginName": "@shopify/cli",
65086518
"pluginType": "core",

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

Lines changed: 14 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
import {globFlags, themeFlags} from '../../flags.js'
2-
import ThemeCommand from '../../utilities/theme-command.js'
3-
import {pull, PullFlags} from '../../services/pull.js'
2+
import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.js'
3+
import {pull} from '../../services/pull.js'
44
import {globalFlags} from '@shopify/cli-kit/node/cli'
55
import {Flags} from '@oclif/core'
66
import {recordTiming} from '@shopify/cli-kit/node/analytics'
7+
import {InferredFlags} from '@oclif/core/interfaces'
8+
import {AdminSession} from '@shopify/cli-kit/node/session'
9+
import {Writable} from 'stream'
710

11+
type PullFlags = InferredFlags<typeof Pull.flags>
812
export default class Pull extends ThemeCommand {
913
static summary = 'Download your remote theme files locally.'
1014

@@ -46,25 +50,16 @@ If no theme is specified, then you're prompted to select the theme to pull from
4650
}),
4751
}
4852

49-
async run(): Promise<void> {
50-
const {flags} = await this.parse(Pull)
51-
const pullFlags: PullFlags = {
52-
path: flags.path,
53-
password: flags.password,
54-
store: flags.store,
55-
theme: flags.theme,
56-
development: flags.development,
57-
live: flags.live,
58-
nodelete: flags.nodelete,
59-
only: flags.only,
60-
ignore: flags.ignore,
61-
force: flags.force,
62-
verbose: flags.verbose,
63-
noColor: flags['no-color'],
64-
}
53+
static multiEnvironmentsFlags: RequiredFlags = ['store', 'password', 'path', ['live', 'development', 'theme']]
6554

55+
async command(
56+
flags: PullFlags,
57+
adminSession?: AdminSession,
58+
multiEnvironment?: boolean,
59+
context?: {stdout?: Writable; stderr?: Writable},
60+
) {
6661
recordTiming('theme-command:pull')
67-
await pull(pullFlags)
62+
await pull({...flags, noColor: flags['no-color']}, adminSession, multiEnvironment, context)
6863
recordTiming('theme-command:pull')
6964
}
7065
}

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

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,14 @@ describe('pull', () => {
7070
// Then
7171
expect(findDevelopmentThemeSpy).not.toHaveBeenCalled()
7272
expect(fetchDevelopmentThemeSpy).toHaveBeenCalledOnce()
73-
expect(downloadTheme).toHaveBeenCalledWith(theme, adminSession, [], localThemeFileSystem, expect.any(Object))
73+
expect(downloadTheme).toHaveBeenCalledWith(
74+
theme,
75+
adminSession,
76+
[],
77+
localThemeFileSystem,
78+
expect.any(Object),
79+
undefined,
80+
)
7481
})
7582

7683
test('should pass the development theme to downloadtheme if development flag is provided', async () => {
@@ -102,7 +109,9 @@ describe('pull', () => {
102109
// Then
103110
expect(vi.mocked(ensureDirectoryConfirmed)).toHaveBeenCalledWith(
104111
false,
105-
'The current Git directory has uncommitted changes. Do you want to proceed?',
112+
'The current Git directory has uncommitted changes.',
113+
undefined,
114+
undefined,
106115
)
107116
})
108117

packages/theme/src/cli/services/pull.ts

Lines changed: 47 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,16 @@ import {glob} from '@shopify/cli-kit/node/fs'
1414
import {cwd} from '@shopify/cli-kit/node/path'
1515
import {insideGitDirectory, isClean} from '@shopify/cli-kit/node/git'
1616
import {recordTiming} from '@shopify/cli-kit/node/analytics'
17+
import {Writable} from 'stream'
1718

1819
interface PullOptions {
1920
path: string
2021
nodelete: boolean
2122
force: boolean
2223
only?: string[]
2324
ignore?: string[]
25+
environment?: string
26+
multiEnvironment?: boolean
2427
}
2528

2629
export interface PullFlags {
@@ -83,6 +86,11 @@ export interface PullFlags {
8386
* Increase the verbosity of the output.
8487
*/
8588
verbose?: boolean
89+
90+
/**
91+
* The environment to pull from.
92+
*/
93+
environment?: string[]
8694
}
8795

8896
/**
@@ -91,19 +99,25 @@ export interface PullFlags {
9199
*
92100
* @param flags - All flags are optional.
93101
*/
94-
export async function pull(flags: PullFlags): Promise<void> {
102+
export async function pull(
103+
flags: PullFlags,
104+
session?: AdminSession,
105+
multiEnvironment?: boolean,
106+
context?: {stdout?: Writable; stderr?: Writable},
107+
): Promise<void> {
95108
recordTiming('theme-service:pull:setup')
96109
configureCLIEnvironment({verbose: flags.verbose, noColor: flags.noColor})
97110

98-
const store = ensureThemeStore({store: flags.store})
99-
const adminSession = await ensureAuthenticatedThemes(store, flags.password)
111+
// when pull is used programmatically, we don't have an admin session, so need to create one
112+
const adminSession =
113+
session ?? (await ensureAuthenticatedThemes(ensureThemeStore({store: flags.store}), flags.password))
100114

101115
const developmentThemeManager = new DevelopmentThemeManager(adminSession)
102116
const developmentTheme = await (flags.development ? developmentThemeManager.find() : developmentThemeManager.fetch())
103117

104-
const {path, nodelete, live, development, only, ignore, force} = flags
118+
const {path, nodelete, live, development, only, ignore, force, environment} = flags
105119

106-
if (!(await validateDirectory(path ?? cwd(), force ?? false))) {
120+
if (!(await validateDirectory(path ?? cwd(), force ?? false, environment?.[0], multiEnvironment))) {
107121
return
108122
}
109123

@@ -116,13 +130,20 @@ export async function pull(flags: PullFlags): Promise<void> {
116130
})
117131
recordTiming('theme-service:pull:setup')
118132

119-
await executePull(theme, adminSession, {
120-
path: path ?? cwd(),
121-
nodelete: nodelete ?? false,
122-
only: only ?? [],
123-
ignore: ignore ?? [],
124-
force: force ?? false,
125-
})
133+
await executePull(
134+
theme,
135+
adminSession,
136+
{
137+
environment: environment?.[0],
138+
force: force ?? false,
139+
ignore: ignore ?? [],
140+
multiEnvironment,
141+
nodelete: nodelete ?? false,
142+
only: only ?? [],
143+
path: path ?? cwd(),
144+
},
145+
context,
146+
)
126147
}
127148

128149
/**
@@ -132,7 +153,12 @@ export async function pull(flags: PullFlags): Promise<void> {
132153
* @param session - the admin session to access the API and download the theme
133154
* @param options - the options that modify how the theme gets downloaded
134155
*/
135-
async function executePull(theme: Theme, session: AdminSession, options: PullOptions) {
156+
async function executePull(
157+
theme: Theme,
158+
session: AdminSession,
159+
options: PullOptions,
160+
context?: {stdout?: Writable; stderr?: Writable},
161+
) {
136162
recordTiming('theme-service:pull:file-system')
137163
const themeFileSystem = mountThemeFileSystem(options.path, {filters: options})
138164
const [remoteChecksums] = await Promise.all([fetchChecksums(theme.id, session), themeFileSystem.ready()])
@@ -142,9 +168,11 @@ async function executePull(theme: Theme, session: AdminSession, options: PullOpt
142168
const store = session.storeFqdn
143169
const themeId = theme.id
144170

145-
await downloadTheme(theme, session, themeChecksums, themeFileSystem, options)
171+
await downloadTheme(theme, session, themeChecksums, themeFileSystem, options, context)
146172

173+
const header = options.environment ? `Environment: ${options.environment}` : ''
147174
renderSuccess({
175+
headline: header,
148176
body: ['The theme', ...themeComponent(theme), 'has been pulled.'],
149177
nextSteps: [
150178
[
@@ -190,7 +218,7 @@ export async function isEmptyDir(path: string) {
190218
* @param force - Whether to force the pull operation.
191219
* @returns Whether the directory is valid.
192220
*/
193-
async function validateDirectory(path: string, force: boolean) {
221+
async function validateDirectory(path: string, force: boolean, environment?: string, multiEnvironment?: boolean) {
194222
if (force) return true
195223

196224
/**
@@ -202,7 +230,7 @@ async function validateDirectory(path: string, force: boolean) {
202230
if (
203231
!(await isEmptyDir(path)) &&
204232
!(await hasRequiredThemeDirectories(path)) &&
205-
!(await ensureDirectoryConfirmed(force))
233+
!(await ensureDirectoryConfirmed(force, undefined, environment, multiEnvironment))
206234
) {
207235
return false
208236
}
@@ -217,7 +245,9 @@ async function validateDirectory(path: string, force: boolean) {
217245
dirtyDirectory &&
218246
!(await ensureDirectoryConfirmed(
219247
force,
220-
'The current Git directory has uncommitted changes. Do you want to proceed?',
248+
'The current Git directory has uncommitted changes.',
249+
environment,
250+
multiEnvironment,
221251
))
222252
) {
223253
return false

packages/theme/src/cli/utilities/theme-downloader.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ import {AdminSession} from '@shopify/cli-kit/node/session'
44
import {fetchThemeAssets} from '@shopify/cli-kit/node/themes/api'
55
import {ThemeFileSystem, Theme, Checksum, ThemeAsset} from '@shopify/cli-kit/node/themes/types'
66
import {renderTasks} from '@shopify/cli-kit/node/ui'
7+
import {Writable} from 'stream'
78

89
interface DownloadOptions {
910
nodelete: boolean
11+
multiEnvironment?: boolean
1012
}
1113

1214
export async function downloadTheme(
@@ -15,14 +17,18 @@ export async function downloadTheme(
1517
remoteChecksums: Checksum[],
1618
themeFileSystem: ThemeFileSystem,
1719
options: DownloadOptions,
20+
context?: {stdout?: Writable; stderr?: Writable},
1821
) {
1922
const deleteTasks = buildDeleteTasks(remoteChecksums, themeFileSystem, options)
2023
const downloadTasks = buildDownloadTasks(remoteChecksums, theme, themeFileSystem, session)
2124

22-
const tasks = [...deleteTasks, ...downloadTasks]
25+
const tasks = [...deleteTasks, ...downloadTasks, {title: `Theme download complete`, task: async () => {}}]
2326

2427
if (tasks.length > 0) {
25-
await renderTasks(tasks)
28+
await renderTasks(tasks, {
29+
renderOptions: {stdout: (context?.stdout ?? process.stdout) as NodeJS.WriteStream},
30+
noProgressBar: options.multiEnvironment,
31+
})
2632
}
2733
}
2834

@@ -65,7 +71,7 @@ function buildDownloadTasks(
6571
const filenames = checksums.map((checksum) => checksum.key)
6672

6773
const getProgress = (params: {current: number; total: number}) =>
68-
`[${Math.round((params.current / params.total) * 100)}%]`
74+
params.total === 0 ? `[100%]` : `[${Math.round((params.current / params.total) * 100)}%]`
6975

7076
const batches = batchedTasks(filenames, MAX_GRAPHQL_THEME_FILES, (batchedFilenames, i) => {
7177
const title = `Downloading files from remote theme ${getProgress({

0 commit comments

Comments
 (0)