Skip to content

Commit 3461431

Browse files
authored
Merge pull request #6301 from Shopify/jb-multi-env-default-flags
Ensure user provided flag values override defaults in multi env commands
2 parents c81514e + cb170bd commit 3461431

File tree

4 files changed

+165
-8
lines changed

4 files changed

+165
-8
lines changed

.changeset/shy-islands-guess.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'@shopify/cli-kit': minor
3+
'@shopify/theme': minor
4+
---
5+
6+
Ensure user provided flag values override defaults in commands with multiple environments

packages/cli-kit/src/public/node/base-command.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -281,7 +281,7 @@ function reportEnvironmentApplication<
281281
* If we parse using this configuration, the only specified flags will be those
282282
* the user actually passed on the command line.
283283
*/
284-
function noDefaultsOptions<TFlags extends FlagOutput, TGlobalFlags extends FlagOutput, TArgs extends ArgOutput>(
284+
export function noDefaultsOptions<TFlags extends FlagOutput, TGlobalFlags extends FlagOutput, TArgs extends ArgOutput>(
285285
options: Input<TFlags, TGlobalFlags, TArgs> | undefined,
286286
): Input<TFlags, TGlobalFlags, TArgs> | undefined {
287287
if (!options?.flags) return options

packages/theme/src/cli/utilities/theme-command.test.ts

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import {Config, Flags} from '@oclif/core'
55
import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
66
import {loadEnvironment} from '@shopify/cli-kit/node/environments'
77
import {renderConcurrent, renderConfirmationPrompt, renderError} from '@shopify/cli-kit/node/ui'
8+
import {fileExistsSync} from '@shopify/cli-kit/node/fs'
89
import type {Writable} from 'stream'
910

1011
vi.mock('@shopify/cli-kit/node/session')
1112
vi.mock('@shopify/cli-kit/node/environments')
1213
vi.mock('@shopify/cli-kit/node/ui')
1314
vi.mock('./theme-store.js')
15+
vi.mock('@shopify/cli-kit/node/fs')
1416

1517
const CommandConfig = new Config({root: __dirname})
1618

@@ -27,6 +29,14 @@ class TestThemeCommand extends ThemeCommand {
2729
password: Flags.string({
2830
env: 'SHOPIFY_FLAG_PASSWORD',
2931
}),
32+
path: Flags.string({
33+
env: 'SHOPIFY_FLAG_PATH',
34+
default: 'current/working/directory',
35+
}),
36+
'no-color': Flags.boolean({
37+
env: 'SHOPIFY_FLAG_NO_COLOR',
38+
default: false,
39+
}),
3040
}
3141

3242
static multiEnvironmentsFlags: RequiredFlags = ['store']
@@ -119,7 +129,9 @@ describe('ThemeCommand', () => {
119129
await command.run()
120130

121131
// Then
122-
expect(loadEnvironment).toHaveBeenCalledWith('development', 'shopify.theme.toml', {from: undefined})
132+
expect(loadEnvironment).toHaveBeenCalledWith('development', 'shopify.theme.toml', {
133+
from: 'current/working/directory',
134+
})
123135
expect(ensureAuthenticatedThemes).toHaveBeenCalledTimes(1)
124136
expect(renderConcurrent).not.toHaveBeenCalled()
125137
expect(command.commandCalls).toHaveLength(1)
@@ -149,8 +161,14 @@ describe('ThemeCommand', () => {
149161
await command.run()
150162

151163
// Then
152-
expect(loadEnvironment).toHaveBeenCalledWith('development', 'shopify.theme.toml', {from: undefined, silent: true})
153-
expect(loadEnvironment).toHaveBeenCalledWith('staging', 'shopify.theme.toml', {from: undefined, silent: true})
164+
expect(loadEnvironment).toHaveBeenCalledWith('development', 'shopify.theme.toml', {
165+
from: 'current/working/directory',
166+
silent: true,
167+
})
168+
expect(loadEnvironment).toHaveBeenCalledWith('staging', 'shopify.theme.toml', {
169+
from: 'current/working/directory',
170+
silent: true,
171+
})
154172

155173
expect(renderConcurrent).toHaveBeenCalledOnce()
156174
expect(renderConcurrent).toHaveBeenCalledWith(
@@ -433,5 +451,106 @@ describe('ThemeCommand', () => {
433451
}),
434452
)
435453
})
454+
455+
test('commands should display an error if the --path flag is used', async () => {
456+
// Given
457+
const environmentConfig = {store: 'store.myshopify.com'}
458+
vi.mocked(loadEnvironment).mockResolvedValue(environmentConfig)
459+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true)
460+
vi.mocked(fileExistsSync).mockReturnValue(true)
461+
462+
await CommandConfig.load()
463+
const command = new TestThemeCommand(
464+
['--environment', 'command-error', '--environment', 'development', '--path', 'path'],
465+
CommandConfig,
466+
)
467+
468+
// When
469+
await command.run()
470+
471+
// Then
472+
expect(renderError).toHaveBeenCalledWith(
473+
expect.objectContaining({
474+
body: [
475+
"Can't use `--path` flag with multiple environments.",
476+
"Configure each environment's theme path in your shopify.theme.toml file instead.",
477+
],
478+
}),
479+
)
480+
})
481+
482+
test('commands should display an error if the --path flag is used and no shopify.theme.toml is found', async () => {
483+
// Given
484+
const environmentConfig = {store: 'store.myshopify.com'}
485+
vi.mocked(loadEnvironment).mockResolvedValue(environmentConfig)
486+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true)
487+
vi.mocked(fileExistsSync).mockReturnValue(false)
488+
489+
await CommandConfig.load()
490+
const command = new TestThemeCommand(
491+
['--environment', 'command-error', '--environment', 'development', '--path', 'path'],
492+
CommandConfig,
493+
)
494+
495+
// When
496+
await command.run()
497+
498+
// Then
499+
expect(renderError).toHaveBeenCalledWith(
500+
expect.objectContaining({
501+
body: [
502+
"Can't use `--path` flag with multiple environments.",
503+
'Run this command from the directory containing shopify.theme.toml.',
504+
'No shopify.theme.toml found in current directory.',
505+
],
506+
}),
507+
)
508+
})
509+
510+
test('CLI and shopify.theme.toml flag values take precedence over defaults', async () => {
511+
// Given
512+
vi.mocked(loadEnvironment)
513+
.mockResolvedValueOnce({store: 'store1.myshopify.com', theme: 'theme1.myshopify.com', path: 'theme/path'})
514+
.mockResolvedValueOnce({store: 'store2.myshopify.com', development: true, path: 'development/path'})
515+
.mockResolvedValueOnce({store: 'store3.myshopify.com', live: true, 'no-color': false})
516+
517+
vi.mocked(renderConcurrent).mockImplementation(async ({processes}) => {
518+
for (const process of processes) {
519+
// eslint-disable-next-line no-await-in-loop
520+
await process.action({} as Writable, {} as Writable, {} as any)
521+
}
522+
})
523+
524+
await CommandConfig.load()
525+
const command = new TestThemeCommand(
526+
['--environment', 'theme', '--environment', 'development', '--environment', 'live', '--no-color'],
527+
CommandConfig,
528+
)
529+
530+
// When
531+
await command.run()
532+
533+
// Then
534+
const commandCalls = command.commandCalls
535+
expect(commandCalls).toHaveLength(3)
536+
537+
const themeEnvFlags = commandCalls[0]?.flags
538+
expect(themeEnvFlags?.path).toEqual('theme/path')
539+
expect(themeEnvFlags?.store).toEqual('store1.myshopify.com')
540+
expect(themeEnvFlags?.theme).toEqual('theme1.myshopify.com')
541+
expect(themeEnvFlags?.['no-color']).toEqual(true)
542+
543+
const developmentEnvFlags = commandCalls[1]?.flags
544+
expect(developmentEnvFlags?.path).toEqual('development/path')
545+
expect(developmentEnvFlags?.store).toEqual('store2.myshopify.com')
546+
expect(developmentEnvFlags?.development).toEqual(true)
547+
expect(developmentEnvFlags?.['no-color']).toEqual(true)
548+
549+
const liveEnvFlags = commandCalls[2]?.flags
550+
expect(liveEnvFlags?.path).toEqual('current/working/directory')
551+
expect(liveEnvFlags?.store).toEqual('store3.myshopify.com')
552+
expect(liveEnvFlags?.live).toEqual(true)
553+
expect(liveEnvFlags?.['no-color']).toEqual(true)
554+
})
436555
})
437556
})

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import {configurationFileName} from '../constants.js'
33
import {useThemeStoreContext} from '../services/local-storage.js'
44
import {hashString} from '@shopify/cli-kit/node/crypto'
55
import {Input} from '@oclif/core/interfaces'
6-
import Command, {ArgOutput, FlagOutput} from '@shopify/cli-kit/node/base-command'
6+
import Command, {ArgOutput, FlagOutput, noDefaultsOptions} from '@shopify/cli-kit/node/base-command'
77
import {AdminSession, ensureAuthenticatedThemes} from '@shopify/cli-kit/node/session'
88
import {loadEnvironment} from '@shopify/cli-kit/node/environments'
99
import {
@@ -16,6 +16,8 @@ import {
1616
import {AbortController} from '@shopify/cli-kit/node/abort'
1717
import {recordEvent, compileData} from '@shopify/cli-kit/node/analytics'
1818
import {addPublicMetadata, addSensitiveMetadata} from '@shopify/cli-kit/node/metadata'
19+
import {cwd, joinPath} from '@shopify/cli-kit/node/path'
20+
import {fileExistsSync} from '@shopify/cli-kit/node/fs'
1921
import type {Writable} from 'stream'
2022

2123
export interface FlagValues {
@@ -103,7 +105,13 @@ export default abstract class ThemeCommand extends Command {
103105
return
104106
}
105107

106-
const environmentsMap = await this.loadEnvironments(environments, flags)
108+
const {flags: flagsWithoutDefaults} = await this.parse(noDefaultsOptions(klass), this.argv)
109+
if ('path' in flagsWithoutDefaults) {
110+
this.errorOnGlobalPath()
111+
return
112+
}
113+
114+
const environmentsMap = await this.loadEnvironments(environments, flags, flagsWithoutDefaults)
107115
const validationResults = await this.validateEnvironments(environmentsMap, requiredFlags)
108116

109117
const commandAllowsForceFlag = 'force' in klass.flags
@@ -119,12 +127,14 @@ export default abstract class ThemeCommand extends Command {
119127
/**
120128
* Create a map of environments from the shopify.theme.toml file
121129
* @param environments - Names of environments to load
122-
* @param flags - Flags provided via the CLI
130+
* @param flags - Flags provided via the CLI or by default
131+
* @param flagsWithoutDefaults - Flags provided via the CLI
123132
* @returns The map of environments
124133
*/
125134
private async loadEnvironments(
126135
environments: EnvironmentName[],
127136
flags: FlagValues,
137+
flagsWithoutDefaults: FlagValues,
128138
): Promise<Map<EnvironmentName, FlagValues>> {
129139
const environmentMap = new Map<EnvironmentName, FlagValues>()
130140

@@ -136,8 +146,9 @@ export default abstract class ThemeCommand extends Command {
136146
})
137147

138148
environmentMap.set(environmentName, {
139-
...environmentFlags,
140149
...flags,
150+
...environmentFlags,
151+
...flagsWithoutDefaults,
141152
environment: [environmentName],
142153
})
143154
}
@@ -323,6 +334,27 @@ export default abstract class ThemeCommand extends Command {
323334
return true
324335
}
325336

337+
/**
338+
* Error if the --path flag is provided via CLI when running a multi environment command
339+
* Commands that act on local files require each environment to specify its own path in the shopify.theme.toml
340+
*/
341+
private errorOnGlobalPath() {
342+
const tomlPath = joinPath(cwd(), 'shopify.theme.toml')
343+
const tomlInCwd = fileExistsSync(tomlPath)
344+
345+
renderError({
346+
body: [
347+
"Can't use `--path` flag with multiple environments.",
348+
...(tomlInCwd
349+
? ["Configure each environment's theme path in your shopify.theme.toml file instead."]
350+
: [
351+
'Run this command from the directory containing shopify.theme.toml.',
352+
'No shopify.theme.toml found in current directory.',
353+
]),
354+
],
355+
})
356+
}
357+
326358
private async logAnalyticsData(session: AdminSession): Promise<void> {
327359
const data = compileData()
328360

0 commit comments

Comments
 (0)