diff --git a/.changeset/theme-check-json-contract.md b/.changeset/theme-check-json-contract.md new file mode 100644 index 00000000000..4782eab7bab --- /dev/null +++ b/.changeset/theme-check-json-contract.md @@ -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. diff --git a/docs-shopify.dev/generated/generated_docs_data_v2.json b/docs-shopify.dev/generated/generated_docs_data_v2.json index a7b0525449c..ed5662391ef 100644 --- a/docs-shopify.dev/generated/generated_docs_data_v2.json +++ b/docs-shopify.dev/generated/generated_docs_data_v2.json @@ -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", @@ -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 '?: 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 '?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment '?: string\n\n /**\n * Minimum severity for exit with error code\n * @environment SHOPIFY_FLAG_FAIL_LEVEL\n */\n '--fail-level '?: 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 '?: 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 '?: 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 '?: 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 '?: string\n\n /**\n * The environment to apply to the current command.\n * @environment SHOPIFY_FLAG_ENVIRONMENT\n */\n '-e, --environment '?: string\n\n /**\n * Minimum severity for exit with error code\n * @environment SHOPIFY_FLAG_FAIL_LEVEL\n */\n '--fail-level '?: 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 '?: 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 '?: 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": { diff --git a/packages/cli-kit/src/public/node/base-command.ts b/packages/cli-kit/src/public/node/base-command.ts index 8a23e55beb2..8f05c1fbeb6 100644 --- a/packages/cli-kit/src/public/node/base-command.ts +++ b/packages/cli-kit/src/public/node/base-command.ts @@ -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' @@ -73,11 +79,19 @@ abstract class BaseCommand extends Command { return undefined } + private jsonOutputFromEnvironment = false + async catch(error: Error & {skipOclifErrorHandling: boolean}): Promise { - 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(): Promise { @@ -250,6 +264,12 @@ ${explanation} in non-interactive terminal environments, such as a CI environmen // command-line arguments. const noDefaultsResult = await super.parse(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. @@ -261,12 +281,18 @@ ${explanation} in non-interactive terminal environments, such as a CI environmen ]) // Report successful application of the environment. - reportEnvironmentApplication( - noDefaultsResult.flags, - result.flags, - isDefaultEnvironment ? 'default' : (environments[0] as string), - environment, - ) + const report = () => + reportEnvironmentApplication( + 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 } diff --git a/packages/cli-kit/src/public/node/error/index.test.ts b/packages/cli-kit/src/public/node/error/index.test.ts index 9958fe57271..142ceba9bf3 100644 --- a/packages/cli-kit/src/public/node/error/index.test.ts +++ b/packages/cli-kit/src/public/node/error/index.test.ts @@ -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' @@ -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) diff --git a/packages/cli-kit/src/public/node/error/index.ts b/packages/cli-kit/src/public/node/error/index.ts index 2b608baddb7..b136f1ba440 100644 --- a/packages/cli-kit/src/public/node/error/index.ts +++ b/packages/cli-kit/src/public/node/error/index.ts @@ -150,7 +150,8 @@ export async function handler(error: unknown): Promise { } 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() diff --git a/packages/cli/README.md b/packages/cli/README.md index 3b91155b56f..7992da27634 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -11563,8 +11563,8 @@ Validate the theme. ``` USAGE $ shopify theme check [--auth-alias ] [-a] [-C ] [-e ...] [--fail-level - crash|error|suggestion|style|warning|info] [--init] [--json-schema] [--list] [--no-color] [--no-input] [-o - text|json] [--path ] [--print] [--verbose] [-v] + crash|error|suggestion|style|warning|info] [-j | --init | -v | --print | --list] [--json-schema] [--no-color] + [--no-input] [-o text|json] [--path ] [--verbose] FLAGS -C, --config= @@ -11582,6 +11582,10 @@ FLAGS The environment to apply to the current command. [env: SHOPIFY_FLAG_ENVIRONMENT] + -j, --json + Output the result as JSON. Automatically disables color output. + [env: SHOPIFY_FLAG_JSON] + -o, --output=