Skip to content
5 changes: 5 additions & 0 deletions .changeset/theme-check-json-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli': major
---

Return `valid` and flat `issues` with camelCase zero-based positions from Theme Check JSON output, retaining every requested environment and failing blocking validations.
11 changes: 10 additions & 1 deletion docs-shopify.dev/generated/generated_docs_data_v2.json
Original file line number Diff line number Diff line change
Expand Up @@ -8427,6 +8427,15 @@
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-check.interface.ts",
"syntaxKind": "PropertySignature",
"name": "-j, --json",
"value": "''",
"description": "Output the result as JSON. Automatically disables color output.",
"isOptional": true,
"environmentValue": "SHOPIFY_FLAG_JSON"
},
{
"filePath": "docs-shopify.dev/commands/interfaces/theme-check.interface.ts",
"syntaxKind": "PropertySignature",
Expand All @@ -8446,7 +8455,7 @@
"environmentValue": "SHOPIFY_FLAG_VERSION"
}
],
"value": "export interface themecheck {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * Automatically fix offenses\n * @environment SHOPIFY_FLAG_AUTO_CORRECT\n */\n '-a, --auto-correct'?: ''\n\n /**\n * Use the config provided, overriding .theme-check.yml if present\n Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported \n * @environment SHOPIFY_FLAG_CONFIG\n */\n '-C, --config <value>'?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Minimum severity for exit with error code\n * @environment SHOPIFY_FLAG_FAIL_LEVEL\n */\n '--fail-level <value>'?: string\n\n /**\n * Generate a .theme-check.yml file\n * @environment SHOPIFY_FLAG_INIT\n */\n '--init'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * List enabled checks\n * @environment SHOPIFY_FLAG_LIST\n */\n '--list'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * The output format to use\n * @environment SHOPIFY_FLAG_OUTPUT\n */\n '-o, --output <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Output active config to STDOUT\n * @environment SHOPIFY_FLAG_PRINT\n */\n '--print'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Print Theme Check version\n * @environment SHOPIFY_FLAG_VERSION\n */\n '-v, --version'?: ''\n}"
"value": "export interface themecheck {\n /**\n * Alias of the Shopify account to use for authentication.\n * @environment SHOPIFY_FLAG_AUTH_ALIAS\n */\n '--auth-alias <value>'?: string\n\n /**\n * Automatically fix offenses\n * @environment SHOPIFY_FLAG_AUTO_CORRECT\n */\n '-a, --auto-correct'?: ''\n\n /**\n * Use the config provided, overriding .theme-check.yml if present\n Supports all theme-check: config values, e.g., theme-check:theme-app-extension,\n theme-check:recommended, theme-check:all\n For backwards compatibility, :theme_app_extension is also supported \n * @environment SHOPIFY_FLAG_CONFIG\n */\n '-C, --config <value>'?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment <value>'?: string\n\n /**\n * Minimum severity for exit with error code\n * @environment SHOPIFY_FLAG_FAIL_LEVEL\n */\n '--fail-level <value>'?: string\n\n /**\n * Generate a .theme-check.yml file\n * @environment SHOPIFY_FLAG_INIT\n */\n '--init'?: ''\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Print the command's JSON schemas.\n * @environment SHOPIFY_FLAG_JSON_SCHEMA\n */\n '--json-schema'?: ''\n\n /**\n * List enabled checks\n * @environment SHOPIFY_FLAG_LIST\n */\n '--list'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * Disable interactive prompts and browser authentication.\n * @environment SHOPIFY_FLAG_NO_INPUT\n */\n '--no-input'?: ''\n\n /**\n * The output format to use\n * @environment SHOPIFY_FLAG_OUTPUT\n */\n '-o, --output <value>'?: string\n\n /**\n * The path where you want to run the command. Defaults to the current working directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Output active config to STDOUT\n * @environment SHOPIFY_FLAG_PRINT\n */\n '--print'?: ''\n\n /**\n * Increase the verbosity of the output. May include sensitive data.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n\n /**\n * Print Theme Check version\n * @environment SHOPIFY_FLAG_VERSION\n */\n '-v, --version'?: ''\n}"
}
},
"themeconsole": {
Expand Down
48 changes: 37 additions & 11 deletions packages/cli-kit/src/public/node/base-command.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import {isDevelopment} from './context/local.js'
import {addPublicMetadata} from './metadata.js'
import {AbortError} from './error.js'
import {commandEventOutputMode, emitCommandEvent, runWithCommandEventsForCommand} from './command-events.js'
import {
commandEventOutputMode,
emitCommandEvent,
renderCommandEventAsJson,
runWithCommandEvents,
runWithCommandEventsForCommand,
} from './command-events.js'
import {outputContent, outputResult, outputToken} from './output.js'
import {setCurrentSessionAlias} from './session.js'
import {terminalSupportsPrompting} from './system.js'
Expand Down Expand Up @@ -73,11 +79,19 @@ abstract class BaseCommand extends Command {
return undefined
}

private jsonOutputFromEnvironment = false

async catch(error: Error & {skipOclifErrorHandling: boolean}): Promise<void> {
error.skipOclifErrorHandling = true
const {errorHandler} = await import('./error-handler.js')
await errorHandler(error, this.config)
return Errors.handle(error)
const handleError = async () => {
error.skipOclifErrorHandling = true
const {errorHandler} = await import('./error-handler.js')
await errorHandler(error, this.config)
return Errors.handle(error)
}
if (this.jsonOutputFromEnvironment && commandEventOutputMode() !== 'json') {
return runWithCommandEvents({outputMode: 'json', sink: renderCommandEventAsJson}, handleError)
}
return handleError()
}

protected async _run<T>(): Promise<T> {
Expand Down Expand Up @@ -250,6 +264,12 @@ ${explanation} in non-interactive terminal environments, such as a CI environmen
// command-line arguments.
const noDefaultsResult = await super.parse<TFlags, TGlobalFlags, TArgs>(noDefaultsOptions(options), argv)

// Remember the requested format before environment flag validation can fail.
const effectiveFlags = {...environment, ...noDefaultsResult.flags}
this.jsonOutputFromEnvironment =
(commandSupportsFlag(options?.flags, 'json') && effectiveFlags.json === true) ||
(commandSupportsFlag(options?.flags, 'output') && effectiveFlags.output === 'json')

// Add the environment's settings to argv and pass them to `super.parse`. This
// invokes oclif's validation system without breaking the oclif black box.
// Replace the original result with this one.
Expand All @@ -261,12 +281,18 @@ ${explanation} in non-interactive terminal environments, such as a CI environmen
])

// Report successful application of the environment.
reportEnvironmentApplication<TFlags, TGlobalFlags, TArgs>(
noDefaultsResult.flags,
result.flags,
isDefaultEnvironment ? 'default' : (environments[0] as string),
environment,
)
const report = () =>
reportEnvironmentApplication<TFlags, TGlobalFlags, TArgs>(
noDefaultsResult.flags,
result.flags,
isDefaultEnvironment ? 'default' : (environments[0] as string),
environment,
)
if ((result.flags.json || result.flags.output === 'json') && commandEventOutputMode() !== 'json') {
runWithCommandEvents({outputMode: 'json', sink: renderCommandEventAsJson}, report)
} else {
report()
}

return result
}
Expand Down
10 changes: 10 additions & 0 deletions packages/cli-kit/src/public/node/error/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
import {jsonOutputEnabled} from '../environment.js'
import {renderFatalError} from '../ui.js'
import {mockAndCaptureOutput} from '../testing/output.js'
import {runWithCommandEvents} from '../command-events.js'
import {ClientError} from 'graphql-request'
import {beforeEach, describe, expect, test, vi} from 'vitest'

Expand Down Expand Up @@ -90,6 +91,15 @@ describe('handler', () => {
expect(renderFatalError).not.toHaveBeenCalled()
})

test('renders JSON when the command selects JSON output without a global flag', async () => {
const output = mockAndCaptureOutput()

await runWithCommandEvents({outputMode: 'json', sink: () => {}}, () => handler(new AbortError('Expected failure')))

expect(JSON.parse(output.info())).toStrictEqual({error: {type: 'abort', message: 'Expected failure'}})
expect(renderFatalError).not.toHaveBeenCalled()
})

test('keeps JSON silent aborts silent', async () => {
vi.mocked(jsonOutputEnabled).mockReturnValue(true)

Expand Down
3 changes: 2 additions & 1 deletion packages/cli-kit/src/public/node/error/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,8 @@ export async function handler(error: unknown): Promise<unknown> {
}

const {jsonOutputEnabled} = await import('../environment.js')
if (jsonOutputEnabled()) {
const {commandEventOutputMode} = await import('../../../private/node/command-event-context.js')
if (jsonOutputEnabled() || commandEventOutputMode() === 'json') {
const {renderFatalErrorAsJson} = await import('../../../private/node/json-error.js')
renderFatalErrorAsJson(fatal)
await flushStdout()
Expand Down
Loading
Loading