Skip to content

Commit 8edc575

Browse files
authored
Merge pull request #6313 from Shopify/jb-multi-env-check
Allow theme check command to be called with multiple environments
2 parents 3ff3c45 + 199fa91 commit 8edc575

File tree

6 files changed

+77
-33
lines changed

6 files changed

+77
-33
lines changed

.changeset/seven-goats-agree.md

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

packages/cli/oclif.manifest.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5121,6 +5121,9 @@
51215121
"hiddenAliases": [
51225122
],
51235123
"id": "theme:check",
5124+
"multiEnvironmentsFlags": [
5125+
"path"
5126+
],
51245127
"pluginAlias": "@shopify/cli",
51255128
"pluginName": "@shopify/cli",
51265129
"pluginType": "core",

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

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@ import {themeFlags} from '../../flags.js'
22
import {
33
formatOffensesJson,
44
formatSummary,
5-
handleExit,
65
initConfig,
76
outputActiveChecks,
87
outputActiveConfig,
98
performAutoFixes,
109
renderOffensesText,
1110
sortOffenses,
1211
isExtendedWriteStream,
12+
handleExit,
1313
type FailLevel,
1414
} from '../../services/check.js'
15-
import ThemeCommand from '../../utilities/theme-command.js'
15+
import ThemeCommand, {RequiredFlags} from '../../utilities/theme-command.js'
1616
import {Flags} from '@oclif/core'
1717
import {globalFlags} from '@shopify/cli-kit/node/cli'
1818
import {outputResult, outputDebug} from '@shopify/cli-kit/node/output'
@@ -21,7 +21,10 @@ import {themeCheckRun, LegacyIdentifiers} from '@shopify/theme-check-node'
2121
import {findPathUp} from '@shopify/cli-kit/node/fs'
2222
import {moduleDirectory, joinPath} from '@shopify/cli-kit/node/path'
2323
import {getPackageVersion} from '@shopify/cli-kit/node/node-package-manager'
24+
import {InferredFlags} from '@oclif/core/interfaces'
25+
import {AdminSession} from '@shopify/cli-kit/node/session'
2426

27+
type CheckFlags = InferredFlags<typeof Check.flags>
2528
export default class Check extends ThemeCommand {
2629
static summary = 'Validate the theme.'
2730

@@ -86,11 +89,12 @@ export default class Check extends ThemeCommand {
8689
environment: themeFlags.environment,
8790
}
8891

89-
async run(): Promise<void> {
90-
const {flags} = await this.parse(Check)
92+
static multiEnvironmentsFlags: RequiredFlags = ['path']
9193

94+
async command(flags: CheckFlags, _session: AdminSession, multiEnvironment: boolean): Promise<void> {
9295
// Its not clear to typescript that path will always be defined
9396
const path = flags.path
97+
const environment = flags.environment?.[0]
9498
// To support backwards compatibility for legacy configs
9599
const isLegacyConfig = flags.config?.startsWith(':') && LegacyIdentifiers.has(flags.config.slice(1))
96100
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
@@ -121,30 +125,32 @@ export default class Check extends ThemeCommand {
121125
}
122126

123127
if (flags.print) {
124-
await outputActiveConfig(path, config)
128+
await outputActiveConfig(path, config, environment)
125129

126130
// --print should not trigger full theme check operation
127131
return
128132
}
129133

130134
if (flags.list) {
131-
await outputActiveChecks(path, config)
135+
await outputActiveChecks(path, config, environment)
132136

133137
// --list should not trigger full theme check operation
134138
return
135139
}
136140

137-
const {offenses, theme} = await runThemeCheck(path, flags.output, config)
141+
const {offenses, theme} = await runThemeCheck(path, flags.output, config, environment)
138142

139143
if (flags['auto-correct']) {
140144
await performAutoFixes(theme, offenses)
141145
}
142146

143-
return handleExit(offenses, flags['fail-level'] as FailLevel)
147+
if (!multiEnvironment) {
148+
return handleExit(offenses, flags['fail-level'] as FailLevel)
149+
}
144150
}
145151
}
146152

147-
export async function runThemeCheck(path: string, outputFormat: string, config?: string) {
153+
export async function runThemeCheck(path: string, outputFormat: string, config?: string, environment?: string) {
148154
const {offenses, theme} = await themeCheckRun(path, config, (message) => {
149155
if (process.env.SHOPIFY_TMP_FLAG_DEBUG) {
150156
outputDebug(message)
@@ -154,13 +160,13 @@ export async function runThemeCheck(path: string, outputFormat: string, config?:
154160
const offensesByFile = sortOffenses(offenses)
155161

156162
if (outputFormat === 'text') {
157-
renderOffensesText(offensesByFile, path)
163+
renderOffensesText(offensesByFile, path, environment)
158164

159165
// Use renderSuccess when theres no offenses
160166
const render = offenses.length ? renderInfo : renderSuccess
161167

162168
render({
163-
headline: 'Theme Check Summary.',
169+
headline: environment ? `[${environment}] Theme Check Summary.` : 'Theme Check Summary.',
164170
body: formatSummary(offenses, offensesByFile, theme),
165171
})
166172
}
@@ -181,7 +187,7 @@ export async function runThemeCheck(path: string, outputFormat: string, config?:
181187
stdout._handle.setBlocking(true)
182188
}
183189

184-
outputResult(JSON.stringify(formatOffensesJson(offensesByFile)))
190+
outputResult(JSON.stringify(formatOffensesJson(offensesByFile, environment)))
185191
}
186192

187193
return {offenses, theme}

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

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export function formatSummary(offenses: Offense[], offensesByFile: OffenseMap, t
194194
return summary
195195
}
196196

197-
export function renderOffensesText(offensesByFile: OffenseMap, themeRootPath: string) {
197+
export function renderOffensesText(offensesByFile: OffenseMap, themeRootPath: string, environment?: string) {
198198
const fileNames = Object.keys(offensesByFile).sort()
199199

200200
fileNames.forEach((filePath) => {
@@ -203,13 +203,13 @@ export function renderOffensesText(offensesByFile: OffenseMap, themeRootPath: st
203203
const headlineFilePath = filePath.replace(themeRootPath, '').slice(1)
204204

205205
renderInfo({
206-
headline: headlineFilePath,
206+
headline: environment ? `[${environment}] ${headlineFilePath}` : headlineFilePath,
207207
body: formatOffenses(offensesByFile[filePath]!),
208208
})
209209
})
210210
}
211211

212-
export function formatOffensesJson(offensesByFile: OffenseMap): TransformedOffenseMap[] {
212+
export function formatOffensesJson(offensesByFile: OffenseMap, environment?: string): TransformedOffenseMap[] {
213213
return Object.entries(offensesByFile).map(([path, offenses]) => {
214214
const transformedOffenses = offenses.map((offense: Offense) => {
215215
return {
@@ -226,6 +226,7 @@ export function formatOffensesJson(offensesByFile: OffenseMap): TransformedOffen
226226
const counts = countOffenseTypes(offenses)
227227

228228
return {
229+
environment,
229230
path,
230231
offenses: transformedOffenses,
231232
errorCount: counts[Severity.ERROR] ?? 0,
@@ -288,7 +289,7 @@ export async function performAutoFixes(sourceCodes: Theme, offenses: Offense[])
288289
await autofix(sourceCodes, offenses, saveToDiskFixApplicator)
289290
}
290291

291-
export async function outputActiveConfig(themeRoot: string, configPath?: string) {
292+
export async function outputActiveConfig(themeRoot: string, configPath?: string, environment?: string) {
292293
const {ignore, settings, rootUri} = await loadConfig(configPath, themeRoot)
293294

294295
const config = {
@@ -304,10 +305,11 @@ export async function outputActiveConfig(themeRoot: string, configPath?: string)
304305
// Dump out the active settings for all checks.
305306
...settings,
306307
}
307-
outputResult(YAML.stringify(config))
308+
const output = environment ? {[environment]: config} : config
309+
outputResult(YAML.stringify(output))
308310
}
309311

310-
export async function outputActiveChecks(root: string, configPath?: string) {
312+
export async function outputActiveChecks(root: string, configPath?: string, environment?: string) {
311313
const {settings, ignore, checks} = await loadConfig(configPath, root)
312314
// Depending on how the configs were merged during loadConfig, there may be
313315
// duplicate patterns to ignore. We can clean them before outputting.
@@ -343,7 +345,8 @@ export async function outputActiveChecks(root: string, configPath?: string) {
343345
return acc
344346
}, {})
345347

346-
outputResult(YAML.stringify(checksList))
348+
const output = environment ? {[environment]: checksList} : checksList
349+
outputResult(YAML.stringify(output))
347350
}
348351

349352
interface ExtendedWriteStream extends NodeJS.WriteStream {

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

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,9 @@ class TestThemeCommandWithUnionFlags extends TestThemeCommand {
8585
}),
8686
}
8787
}
88+
class TestThemeCommandWithPath extends TestThemeCommand {
89+
static multiEnvironmentsFlags: RequiredFlags = ['store', 'path']
90+
}
8891

8992
class TestUnauthenticatedThemeCommand extends ThemeCommand {
9093
static flags = {
@@ -402,6 +405,31 @@ describe('ThemeCommand', () => {
402405
expect(renderConcurrentProcesses?.map((process) => process.prefix)).toEqual(['development', 'production'])
403406
})
404407

408+
test('should not execute commands in environments that are missing required flags even if they have a default value', async () => {
409+
// Given
410+
vi.mocked(loadEnvironment)
411+
.mockResolvedValueOnce({store: 'store1.myshopify.com', path: '/a/path'})
412+
.mockResolvedValueOnce({})
413+
.mockResolvedValueOnce({store: 'store3.myshopify.com'})
414+
415+
vi.mocked(renderConfirmationPrompt).mockResolvedValue(true)
416+
vi.mocked(renderConcurrent).mockResolvedValue(undefined)
417+
418+
await CommandConfig.load()
419+
const command = new TestThemeCommandWithPath(
420+
['--environment', 'development', '--environment', 'env-missing-store', '--environment', 'path-defaults-to-cwd'],
421+
CommandConfig,
422+
)
423+
424+
// When
425+
await command.run()
426+
427+
// Then
428+
const renderConcurrentProcesses = vi.mocked(renderConcurrent).mock.calls[0]?.[0]?.processes
429+
expect(renderConcurrentProcesses).toHaveLength(1)
430+
expect(renderConcurrentProcesses?.map((process) => process.prefix)).toEqual(['development'])
431+
})
432+
405433
test('should not execute commands in environments that are missing required "one of" flags', async () => {
406434
// Given
407435
vi.mocked(loadEnvironment)

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

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -142,12 +142,8 @@ export default abstract class ThemeCommand extends Command {
142142
* @param flagsWithoutDefaults - Flags provided via the CLI
143143
* @returns The map of environments
144144
*/
145-
private async loadEnvironments(
146-
environments: EnvironmentName[],
147-
flags: FlagValues,
148-
flagsWithoutDefaults: FlagValues,
149-
): Promise<Map<EnvironmentName, FlagValues>> {
150-
const environmentMap = new Map<EnvironmentName, FlagValues>()
145+
private async loadEnvironments(environments: EnvironmentName[], flags: FlagValues, flagsWithoutDefaults: FlagValues) {
146+
const environmentMap = new Map<EnvironmentName, {flags: FlagValues; validationFlags: FlagValues}>()
151147

152148
for (const environmentName of environments) {
153149
// eslint-disable-next-line no-await-in-loop
@@ -162,10 +158,13 @@ export default abstract class ThemeCommand extends Command {
162158
}
163159

164160
environmentMap.set(environmentName, {
165-
...flags,
166-
...environmentFlags,
167-
...flagsWithoutDefaults,
168-
environment: [environmentName],
161+
flags: {
162+
...flags,
163+
...environmentFlags,
164+
...flagsWithoutDefaults,
165+
environment: [environmentName],
166+
},
167+
validationFlags: {...environmentFlags, ...flagsWithoutDefaults} as FlagValues,
169168
})
170169
}
171170

@@ -180,21 +179,21 @@ export default abstract class ThemeCommand extends Command {
180179
* @returns An object containing valid and invalid environment arrays
181180
*/
182181
private async validateEnvironments(
183-
environmentMap: Map<EnvironmentName, FlagValues>,
182+
environmentMap: Map<EnvironmentName, {flags: FlagValues; validationFlags: FlagValues}>,
184183
requiredFlags: Exclude<RequiredFlags, null>,
185184
requiresAuth: boolean,
186185
) {
187186
const valid: ValidEnvironment[] = []
188187
const invalid: {environment: EnvironmentName; reason: string}[] = []
189188

190-
for (const [environmentName, environmentFlags] of environmentMap) {
191-
const validationResult = this.validConfig(environmentFlags, requiredFlags, environmentName)
189+
for (const [environmentName, {flags, validationFlags}] of environmentMap) {
190+
const validationResult = this.validConfig(validationFlags, requiredFlags, environmentName)
192191
if (validationResult !== true) {
193192
const missingFlagsText = validationResult.join(', ')
194193
invalid.push({environment: environmentName, reason: `Missing flags: ${missingFlagsText}`})
195194
continue
196195
}
197-
valid.push({environment: environmentName, flags: environmentFlags, requiresAuth})
196+
valid.push({environment: environmentName, flags, requiresAuth})
198197
}
199198

200199
return {valid, invalid}

0 commit comments

Comments
 (0)