Skip to content

Commit b1a9aff

Browse files
Add typed JSON output to theme check
1 parent f8f160d commit b1a9aff

9 files changed

Lines changed: 420 additions & 79 deletions

File tree

‎docs-shopify.dev/generated/generated_docs_data_v2.json‎

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6513,6 +6513,15 @@
65136513
"isOptional": true,
65146514
"environmentValue": "SHOPIFY_FLAG_ENVIRONMENT"
65156515
},
6516+
{
6517+
"filePath": "docs-shopify.dev/commands/interfaces/theme-check.interface.ts",
6518+
"syntaxKind": "PropertySignature",
6519+
"name": "-j, --json",
6520+
"value": "''",
6521+
"description": "Output the result as JSON. Automatically disables color output.",
6522+
"isOptional": true,
6523+
"environmentValue": "SHOPIFY_FLAG_JSON"
6524+
},
65166525
{
65176526
"filePath": "docs-shopify.dev/commands/interfaces/theme-check.interface.ts",
65186527
"syntaxKind": "PropertySignature",
@@ -6532,7 +6541,7 @@
65326541
"environmentValue": "SHOPIFY_FLAG_VERSION"
65336542
}
65346543
],
6535-
"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 * 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}"
6544+
"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 * 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}"
65366545
}
65376546
},
65386547
"themeconsole": {

‎packages/cli/README.md‎

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7070,8 +7070,8 @@ Validate the theme.
70707070
```
70717071
USAGE
70727072
$ shopify theme check [--auth-alias <value>] [-a] [-C <value>] [-e <value>...] [--fail-level
7073-
crash|error|suggestion|style|warning|info] [--init] [--json-schema] [--list] [--no-color] [-o text|json] [--path
7074-
<value>] [--print] [--verbose] [-v]
7073+
crash|error|suggestion|style|warning|info] [-j | --init | -v | --print | --list] [--json-schema] [--no-color] [-o
7074+
text|json] [--path <value>] [--verbose]
70757075

70767076
FLAGS
70777077
-C, --config=<value>
@@ -7089,6 +7089,10 @@ FLAGS
70897089
The environment to apply to the current command.
70907090
[env: SHOPIFY_FLAG_ENVIRONMENT]
70917091

7092+
-j, --json
7093+
Output the result as JSON. Automatically disables color output.
7094+
[env: SHOPIFY_FLAG_JSON]
7095+
70927096
-o, --output=<option>
70937097
[default: text] The output format to use
70947098
[env: SHOPIFY_FLAG_OUTPUT]
@@ -7141,6 +7145,133 @@ DESCRIPTION
71417145
Calls and runs "Theme Check" (https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors
71427146
and to ensure that it follows theme and Liquid best practices. "Learn more about the checks that Theme Check runs."
71437147
(https://shopify.dev/docs/themes/tools/theme-check/checks)
7148+
7149+
Output from `--json` conforms to the `ThemeCheckResult` schema.
7150+
7151+
Use `--json-schema` to print the result, error, and event schemas.
7152+
7153+
```json
7154+
{
7155+
"anyOf": [
7156+
{
7157+
"$ref": "#/definitions/ThemeCheckEnvironment/properties/result"
7158+
},
7159+
{
7160+
"type": "object",
7161+
"properties": {
7162+
"environments": {
7163+
"type": "array",
7164+
"items": {
7165+
"$ref": "#/definitions/ThemeCheckEnvironment"
7166+
}
7167+
}
7168+
},
7169+
"required": [
7170+
"environments"
7171+
],
7172+
"additionalProperties": false
7173+
}
7174+
],
7175+
"title": "ThemeCheckResult",
7176+
"definitions": {
7177+
"ThemeCheckOffense": {
7178+
"type": "object",
7179+
"properties": {
7180+
"check": {
7181+
"type": "string"
7182+
},
7183+
"severity": {
7184+
"type": "string",
7185+
"enum": [
7186+
"error",
7187+
"warning",
7188+
"info"
7189+
]
7190+
},
7191+
"start_row": {
7192+
"type": "number"
7193+
},
7194+
"start_column": {
7195+
"type": "number"
7196+
},
7197+
"end_row": {
7198+
"type": "number"
7199+
},
7200+
"end_column": {
7201+
"type": "number"
7202+
},
7203+
"message": {
7204+
"type": "string"
7205+
}
7206+
},
7207+
"required": [
7208+
"check",
7209+
"severity",
7210+
"start_row",
7211+
"start_column",
7212+
"end_row",
7213+
"end_column",
7214+
"message"
7215+
],
7216+
"additionalProperties": false
7217+
},
7218+
"ThemeCheckFile": {
7219+
"type": "object",
7220+
"properties": {
7221+
"environment": {
7222+
"type": "string"
7223+
},
7224+
"path": {
7225+
"type": "string"
7226+
},
7227+
"offenses": {
7228+
"type": "array",
7229+
"items": {
7230+
"$ref": "#/definitions/ThemeCheckOffense"
7231+
}
7232+
},
7233+
"errorCount": {
7234+
"type": "number"
7235+
},
7236+
"warningCount": {
7237+
"type": "number"
7238+
},
7239+
"infoCount": {
7240+
"type": "number"
7241+
}
7242+
},
7243+
"required": [
7244+
"path",
7245+
"offenses",
7246+
"errorCount",
7247+
"warningCount",
7248+
"infoCount"
7249+
],
7250+
"additionalProperties": false
7251+
},
7252+
"ThemeCheckEnvironment": {
7253+
"type": "object",
7254+
"properties": {
7255+
"environment": {
7256+
"type": "string"
7257+
},
7258+
"result": {
7259+
"type": "array",
7260+
"items": {
7261+
"$ref": "#/definitions/ThemeCheckFile"
7262+
}
7263+
}
7264+
},
7265+
"required": [
7266+
"environment",
7267+
"result"
7268+
],
7269+
"additionalProperties": false
7270+
}
7271+
},
7272+
"$schema": "http://json-schema.org/draft-07/schema#"
7273+
}
7274+
```
71447275
```
71457276
71467277
## `shopify theme console`

‎packages/cli/oclif.manifest.json‎

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9350,7 +9350,7 @@
93509350
"args": {
93519351
},
93529352
"customPluginName": "@shopify/theme",
9353-
"description": "Calls and runs \"Theme Check\" (https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. \"Learn more about the checks that Theme Check runs.\" (https://shopify.dev/docs/themes/tools/theme-check/checks)",
9353+
"description": "Calls and runs \"Theme Check\" (https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. \"Learn more about the checks that Theme Check runs.\" (https://shopify.dev/docs/themes/tools/theme-check/checks)\n\nOutput from `--json` conforms to the `ThemeCheckResult` schema.\n\nUse `--json-schema` to print the result, error, and event schemas.\n\n```json\n{\n \"anyOf\": [\n {\n \"$ref\": \"#/definitions/ThemeCheckEnvironment/properties/result\"\n },\n {\n \"type\": \"object\",\n \"properties\": {\n \"environments\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeCheckEnvironment\"\n }\n }\n },\n \"required\": [\n \"environments\"\n ],\n \"additionalProperties\": false\n }\n ],\n \"title\": \"ThemeCheckResult\",\n \"definitions\": {\n \"ThemeCheckOffense\": {\n \"type\": \"object\",\n \"properties\": {\n \"check\": {\n \"type\": \"string\"\n },\n \"severity\": {\n \"type\": \"string\",\n \"enum\": [\n \"error\",\n \"warning\",\n \"info\"\n ]\n },\n \"start_row\": {\n \"type\": \"number\"\n },\n \"start_column\": {\n \"type\": \"number\"\n },\n \"end_row\": {\n \"type\": \"number\"\n },\n \"end_column\": {\n \"type\": \"number\"\n },\n \"message\": {\n \"type\": \"string\"\n }\n },\n \"required\": [\n \"check\",\n \"severity\",\n \"start_row\",\n \"start_column\",\n \"end_row\",\n \"end_column\",\n \"message\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeCheckFile\": {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"path\": {\n \"type\": \"string\"\n },\n \"offenses\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeCheckOffense\"\n }\n },\n \"errorCount\": {\n \"type\": \"number\"\n },\n \"warningCount\": {\n \"type\": \"number\"\n },\n \"infoCount\": {\n \"type\": \"number\"\n }\n },\n \"required\": [\n \"path\",\n \"offenses\",\n \"errorCount\",\n \"warningCount\",\n \"infoCount\"\n ],\n \"additionalProperties\": false\n },\n \"ThemeCheckEnvironment\": {\n \"type\": \"object\",\n \"properties\": {\n \"environment\": {\n \"type\": \"string\"\n },\n \"result\": {\n \"type\": \"array\",\n \"items\": {\n \"$ref\": \"#/definitions/ThemeCheckFile\"\n }\n }\n },\n \"required\": [\n \"environment\",\n \"result\"\n ],\n \"additionalProperties\": false\n }\n },\n \"$schema\": \"http://json-schema.org/draft-07/schema#\"\n}\n```",
93549354
"descriptionWithMarkdown": "Calls and runs [Theme Check](https://shopify.dev/docs/themes/tools/theme-check) to analyze your theme code for errors and to ensure that it follows theme and Liquid best practices. [Learn more about the checks that Theme Check runs.](https://shopify.dev/docs/themes/tools/theme-check/checks)",
93559355
"enableJsonFlag": false,
93569356
"flags": {
@@ -9416,6 +9416,21 @@
94169416
"required": false,
94179417
"type": "boolean"
94189418
},
9419+
"json": {
9420+
"allowNo": false,
9421+
"char": "j",
9422+
"description": "Output the result as JSON. Automatically disables color output.",
9423+
"env": "SHOPIFY_FLAG_JSON",
9424+
"exclusive": [
9425+
"init",
9426+
"version",
9427+
"print",
9428+
"list"
9429+
],
9430+
"hidden": false,
9431+
"name": "json",
9432+
"type": "boolean"
9433+
},
94199434
"json-schema": {
94209435
"allowNo": false,
94219436
"description": "Print the command's JSON schemas.",

‎packages/eslint-plugin-cli/rules/json-output-command-exceptions.js‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,6 @@ const commandExceptions = [
6060
'packages/plugin-did-you-mean/src/commands/config/autocorrect/off.ts',
6161
'packages/plugin-did-you-mean/src/commands/config/autocorrect/on.ts',
6262
'packages/plugin-did-you-mean/src/commands/config/autocorrect/status.ts',
63-
'packages/theme/src/cli/commands/theme/check.ts',
6463
'packages/theme/src/cli/commands/theme/init.ts',
6564
'packages/theme/src/cli/commands/theme/metafields/pull.ts',
6665
'packages/theme/src/cli/commands/theme/package.ts',
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import Check from './check.js'
2+
import {checkTheme, formatOffensesJson, sortOffenses} from '../../services/check.js'
3+
import {themeCheckJsonOutputSchema} from '../../services/check/types.js'
4+
import {encodeThemeCheckResult} from '../../services/check/result.js'
5+
import {captureStandardStreams} from '../../utilities/testing/streams.js'
6+
import {afterEach, expect, test, vi} from 'vitest'
7+
import {themeCheckRun, Severity, SourceCodeType, path as pathUtils} from '@shopify/theme-check-node'
8+
import {inTemporaryDirectory} from '@shopify/cli-kit/node/fs'
9+
10+
vi.mock('@shopify/theme-check-node', async (importOriginal) => ({
11+
...(await importOriginal<typeof import('@shopify/theme-check-node')>()),
12+
themeCheckRun: vi.fn(),
13+
}))
14+
15+
vi.mock('@shopify/cli-kit/node/environments')
16+
17+
const offense = {
18+
type: SourceCodeType.LiquidHtml,
19+
check: 'ExampleCheck',
20+
severity: Severity.ERROR,
21+
uri: 'file:///theme/templates/index.liquid',
22+
start: {index: 0, line: 0, character: 0},
23+
end: {index: 1, line: 0, character: 1},
24+
message: 'Example offense',
25+
}
26+
27+
afterEach(() => vi.unstubAllEnvs())
28+
29+
test('exposes the contract and preserves compact JSON, environment omission and counts', async () => {
30+
expect(Check.jsonOutputSchema).toBe(themeCheckJsonOutputSchema)
31+
vi.mocked(themeCheckRun).mockResolvedValue({offenses: [offense], theme: [], config: {} as never})
32+
const {result} = await checkTheme('/theme')
33+
expect(result).toEqual(formatOffensesJson(sortOffenses([offense])))
34+
expect(encodeThemeCheckResult(result)).toBe(
35+
JSON.stringify([
36+
{
37+
path: pathUtils.fsPath(offense.uri),
38+
offenses: [
39+
{
40+
check: 'ExampleCheck',
41+
severity: 'error',
42+
start_row: 0,
43+
start_column: 0,
44+
end_row: 0,
45+
end_column: 1,
46+
message: 'Example offense',
47+
},
48+
],
49+
errorCount: 1,
50+
warningCount: 0,
51+
infoCount: 0,
52+
},
53+
]),
54+
)
55+
expect(encodeThemeCheckResult([])).toBe('[]')
56+
expect(() => themeCheckJsonOutputSchema.validate([{...result[0], errorCount: '1'}])).toThrow()
57+
expect(() =>
58+
themeCheckJsonOutputSchema.validate([{...result[0], offenses: [{...result[0]!.offenses[0], severity: 'fatal'}]}]),
59+
).toThrow()
60+
})
61+
62+
test.each(['--json', '--output=json'])('writes compact JSON and preserves unsuccessful exit with %s', async (flag) => {
63+
await inTemporaryDirectory(async (directory) => {
64+
vi.stubEnv('SHOPIFY_UNIT_TEST', 'false')
65+
vi.resetModules()
66+
const {default: StreamCheck} = await import('./check.js')
67+
const {themeCheckRun: check} = await import('@shopify/theme-check-node')
68+
const {Config} = await import('@oclif/core')
69+
const config = new Config({root: __dirname})
70+
await config.load()
71+
vi.mocked(check).mockResolvedValue({offenses: [offense], theme: [], config: {} as never})
72+
const exit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never)
73+
const streams = captureStandardStreams()
74+
try {
75+
await new StreamCheck([`--path=${directory}`, flag], config).run()
76+
} finally {
77+
streams.restore()
78+
}
79+
expect(streams.stdout()).toBe(`${JSON.stringify(formatOffensesJson(sortOffenses([offense])))}\n`)
80+
expect(streams.stderr()).toBe('')
81+
expect(exit).toHaveBeenCalledWith(1)
82+
})
83+
})
84+
85+
test.each(['init', 'version', 'print', 'list'])('rejects --json with --%s before execution', async (mode) => {
86+
const {Config} = await import('@oclif/core')
87+
const config = new Config({root: __dirname})
88+
await config.load()
89+
await expect(new Check(['--json', `--${mode}`], config).run()).rejects.toThrow()
90+
expect(themeCheckRun).not.toHaveBeenCalled()
91+
})
92+
93+
test.each(['success', 'partial failure', 'total failure'])(
94+
'collects multiple environments into one document on %s',
95+
async (mode) => {
96+
await inTemporaryDirectory(async (directory) => {
97+
vi.stubEnv('SHOPIFY_UNIT_TEST', 'false')
98+
vi.resetModules()
99+
const {default: StreamCheck} = await import('./check.js')
100+
const {themeCheckRun: check} = await import('@shopify/theme-check-node')
101+
const {Config} = await import('@oclif/core')
102+
const config = new Config({root: __dirname})
103+
await config.load()
104+
vi.mocked(check).mockResolvedValue({offenses: [], theme: [], config: {} as never})
105+
if (mode === 'partial failure') vi.mocked(check).mockRejectedValueOnce(new Error('Check failed'))
106+
if (mode === 'total failure') vi.mocked(check).mockRejectedValue(new Error('Check failed'))
107+
const exit = vi.spyOn(process, 'exit').mockImplementation(() => undefined as never)
108+
const {loadEnvironment} = await import('@shopify/cli-kit/node/environments')
109+
vi.mocked(loadEnvironment).mockResolvedValue({path: directory})
110+
const {runWithCommandEventsForCommand} = await import('@shopify/cli-kit/node/command-events')
111+
const streams = captureStandardStreams()
112+
try {
113+
const argv = ['--environment=first', '--environment=second', '--json']
114+
await runWithCommandEventsForCommand(argv, () => new StreamCheck(argv, config).run())
115+
} finally {
116+
streams.restore()
117+
}
118+
let names = ['first', 'second']
119+
if (mode === 'partial failure') names = ['second']
120+
if (mode === 'total failure') names = []
121+
expect(JSON.parse(streams.stdout())).toEqual({
122+
environments: names.map((environment) => ({environment, result: []})),
123+
})
124+
if (mode.includes('failure')) expect(streams.stderr()).toContain('Check failed')
125+
expect(exit).not.toHaveBeenCalled()
126+
})
127+
},
128+
)

0 commit comments

Comments
 (0)