Skip to content

Commit 7a4c5d2

Browse files
committed
Add --json to validate
First pass, not final agent shape
1 parent c953d80 commit 7a4c5d2

File tree

8 files changed

+144
-7
lines changed

8 files changed

+144
-7
lines changed

docs-shopify.dev/commands/interfaces/app-config-validate.interface.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ export interface appconfigvalidate {
1212
*/
1313
'-c, --config <value>'?: string
1414

15+
/**
16+
* Output the result as JSON. Automatically disables color output.
17+
* @environment SHOPIFY_FLAG_JSON
18+
*/
19+
'-j, --json'?: ''
20+
1521
/**
1622
* Disable color output.
1723
* @environment SHOPIFY_FLAG_NO_COLOR

docs-shopify.dev/generated/generated_docs_data.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -832,9 +832,18 @@
832832
"description": "The name of the app configuration.",
833833
"isOptional": true,
834834
"environmentValue": "SHOPIFY_FLAG_APP_CONFIG"
835+
},
836+
{
837+
"filePath": "docs-shopify.dev/commands/interfaces/app-config-validate.interface.ts",
838+
"syntaxKind": "PropertySignature",
839+
"name": "-j, --json",
840+
"value": "\"\"",
841+
"description": "Output the result as JSON. Automatically disables color output.",
842+
"isOptional": true,
843+
"environmentValue": "SHOPIFY_FLAG_JSON"
835844
}
836845
],
837-
"value": "export interface appconfigvalidate {\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
846+
"value": "export interface appconfigvalidate {\n /**\n * The Client ID of your app.\n * @environment SHOPIFY_FLAG_CLIENT_ID\n */\n '--client-id <value>'?: string\n\n /**\n * The name of the app configuration.\n * @environment SHOPIFY_FLAG_APP_CONFIG\n */\n '-c, --config <value>'?: string\n\n /**\n * Output the result as JSON. Automatically disables color output.\n * @environment SHOPIFY_FLAG_JSON\n */\n '-j, --json'?: ''\n\n /**\n * Disable color output.\n * @environment SHOPIFY_FLAG_NO_COLOR\n */\n '--no-color'?: ''\n\n /**\n * The path to your app directory.\n * @environment SHOPIFY_FLAG_PATH\n */\n '--path <value>'?: string\n\n /**\n * Reset all your settings.\n * @environment SHOPIFY_FLAG_RESET\n */\n '--reset'?: ''\n\n /**\n * Increase the verbosity of the output.\n * @environment SHOPIFY_FLAG_VERBOSE\n */\n '--verbose'?: ''\n}"
838847
}
839848
}
840849
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import Validate from './validate.js'
2+
import {linkedAppContext} from '../../../services/app-context.js'
3+
import {validateApp} from '../../../services/validate.js'
4+
import {testAppLinked} from '../../../models/app/app.test-data.js'
5+
import {describe, expect, test, vi} from 'vitest'
6+
7+
vi.mock('../../../services/app-context.js')
8+
vi.mock('../../../services/validate.js')
9+
10+
describe('app config validate command', () => {
11+
test('calls validateApp with json: false by default', async () => {
12+
// Given
13+
const app = testAppLinked()
14+
vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited<ReturnType<typeof linkedAppContext>>)
15+
vi.mocked(validateApp).mockResolvedValue()
16+
17+
// When
18+
await Validate.run([], import.meta.url)
19+
20+
// Then
21+
expect(validateApp).toHaveBeenCalledWith(app, {json: false})
22+
})
23+
24+
test('calls validateApp with json: true when --json flag is passed', async () => {
25+
// Given
26+
const app = testAppLinked()
27+
vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited<ReturnType<typeof linkedAppContext>>)
28+
vi.mocked(validateApp).mockResolvedValue()
29+
30+
// When
31+
await Validate.run(['--json'], import.meta.url)
32+
33+
// Then
34+
expect(validateApp).toHaveBeenCalledWith(app, {json: true})
35+
})
36+
37+
test('calls validateApp with json: true when -j flag is passed', async () => {
38+
// Given
39+
const app = testAppLinked()
40+
vi.mocked(linkedAppContext).mockResolvedValue({app} as Awaited<ReturnType<typeof linkedAppContext>>)
41+
vi.mocked(validateApp).mockResolvedValue()
42+
43+
// When
44+
await Validate.run(['-j'], import.meta.url)
45+
46+
// Then
47+
expect(validateApp).toHaveBeenCalledWith(app, {json: true})
48+
})
49+
})

packages/app/src/cli/commands/app/config/validate.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import {appFlags} from '../../../flags.js'
22
import {validateApp} from '../../../services/validate.js'
33
import AppLinkedCommand, {AppLinkedCommandOutput} from '../../../utilities/app-linked-command.js'
44
import {linkedAppContext} from '../../../services/app-context.js'
5-
import {globalFlags} from '@shopify/cli-kit/node/cli'
5+
import {globalFlags, jsonFlag} from '@shopify/cli-kit/node/cli'
66

77
export default class Validate extends AppLinkedCommand {
88
static summary = 'Validate your app configuration and extensions.'
@@ -14,6 +14,7 @@ export default class Validate extends AppLinkedCommand {
1414
static flags = {
1515
...globalFlags,
1616
...appFlags,
17+
...jsonFlag,
1718
}
1819

1920
public async run(): Promise<AppLinkedCommandOutput> {
@@ -27,7 +28,7 @@ export default class Validate extends AppLinkedCommand {
2728
unsafeReportMode: true,
2829
})
2930

30-
await validateApp(app)
31+
await validateApp(app, {json: flags.json})
3132

3233
return {app}
3334
}

packages/app/src/cli/services/validate.test.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,17 @@ import {validateApp} from './validate.js'
22
import {testAppLinked} from '../models/app/app.test-data.js'
33
import {AppErrors} from '../models/app/loader.js'
44
import {describe, expect, test, vi} from 'vitest'
5+
import {outputResult} from '@shopify/cli-kit/node/output'
56
import {renderError, renderSuccess} from '@shopify/cli-kit/node/ui'
67
import {AbortSilentError} from '@shopify/cli-kit/node/error'
78

9+
vi.mock('@shopify/cli-kit/node/output', async (importOriginal) => {
10+
const actual = await importOriginal<typeof import('@shopify/cli-kit/node/output')>()
11+
return {
12+
...actual,
13+
outputResult: vi.fn(),
14+
}
15+
})
816
vi.mock('@shopify/cli-kit/node/ui')
917

1018
describe('validateApp', () => {
@@ -18,6 +26,20 @@ describe('validateApp', () => {
1826
// Then
1927
expect(renderSuccess).toHaveBeenCalledWith({headline: 'App configuration is valid.'})
2028
expect(renderError).not.toHaveBeenCalled()
29+
expect(outputResult).not.toHaveBeenCalled()
30+
})
31+
32+
test('outputs json success when --json is enabled and there are no errors', async () => {
33+
// Given
34+
const app = testAppLinked()
35+
36+
// When
37+
await validateApp(app, {json: true})
38+
39+
// Then
40+
expect(outputResult).toHaveBeenCalledWith(JSON.stringify({valid: true, errors: []}, null, 2))
41+
expect(renderSuccess).not.toHaveBeenCalled()
42+
expect(renderError).not.toHaveBeenCalled()
2143
})
2244

2345
test('renders errors and throws when there are validation errors', async () => {
@@ -35,6 +57,31 @@ describe('validateApp', () => {
3557
body: expect.stringContaining('client_id is required'),
3658
})
3759
expect(renderSuccess).not.toHaveBeenCalled()
60+
expect(outputResult).not.toHaveBeenCalled()
61+
})
62+
63+
test('outputs json errors and throws when --json is enabled and there are validation errors', async () => {
64+
// Given
65+
const errors = new AppErrors()
66+
errors.addError('/path/to/shopify.app.toml', 'client_id is required')
67+
errors.addError('/path/to/extensions/my-ext/shopify.extension.toml', 'invalid type "unknown"')
68+
const app = testAppLinked()
69+
app.errors = errors
70+
71+
// When / Then
72+
await expect(validateApp(app, {json: true})).rejects.toThrow(AbortSilentError)
73+
expect(outputResult).toHaveBeenCalledWith(
74+
JSON.stringify(
75+
{
76+
valid: false,
77+
errors: ['client_id is required', 'invalid type "unknown"'],
78+
},
79+
null,
80+
2,
81+
),
82+
)
83+
expect(renderError).not.toHaveBeenCalled()
84+
expect(renderSuccess).not.toHaveBeenCalled()
3885
})
3986

4087
test('renders success when errors object exists but is empty', async () => {
@@ -48,5 +95,6 @@ describe('validateApp', () => {
4895

4996
// Then
5097
expect(renderSuccess).toHaveBeenCalledWith({headline: 'App configuration is valid.'})
98+
expect(outputResult).not.toHaveBeenCalled()
5199
})
52100
})

packages/app/src/cli/services/validate.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,32 @@
11
import {AppLinkedInterface} from '../models/app/app.js'
2-
import {stringifyMessage} from '@shopify/cli-kit/node/output'
2+
import {outputResult, stringifyMessage} from '@shopify/cli-kit/node/output'
33
import {renderError, renderSuccess} from '@shopify/cli-kit/node/ui'
44
import {AbortSilentError} from '@shopify/cli-kit/node/error'
55

6-
export async function validateApp(app: AppLinkedInterface): Promise<void> {
6+
interface ValidateAppOptions {
7+
json: boolean
8+
}
9+
10+
export async function validateApp(app: AppLinkedInterface, options: ValidateAppOptions = {json: false}): Promise<void> {
711
const errors = app.errors
812

913
if (!errors || errors.isEmpty()) {
14+
if (options.json) {
15+
outputResult(JSON.stringify({valid: true, errors: []}, null, 2))
16+
return
17+
}
18+
1019
renderSuccess({headline: 'App configuration is valid.'})
1120
return
1221
}
1322

1423
const errorMessages = errors.toJSON().map((error) => stringifyMessage(error).trim())
1524

25+
if (options.json) {
26+
outputResult(JSON.stringify({valid: false, errors: errorMessages}, null, 2))
27+
throw new AbortSilentError()
28+
}
29+
1630
renderError({
1731
headline: 'Validation errors found.',
1832
body: errorMessages.join('\n\n'),

packages/cli/README.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -312,11 +312,12 @@ Validate your app configuration and extensions.
312312

313313
```
314314
USAGE
315-
$ shopify app config validate [--client-id <value> | -c <value>] [--no-color] [--path <value>] [--reset | ]
316-
[--verbose]
315+
$ shopify app config validate [--client-id <value> | -c <value>] [-j] [--no-color] [--path <value>] [--reset | ]
316+
[--verbose]
317317
318318
FLAGS
319319
-c, --config=<value> [env: SHOPIFY_FLAG_APP_CONFIG] The name of the app configuration.
320+
-j, --json [env: SHOPIFY_FLAG_JSON] Output the result as JSON. Automatically disables color output.
320321
--client-id=<value> [env: SHOPIFY_FLAG_CLIENT_ID] The Client ID of your app.
321322
--no-color [env: SHOPIFY_FLAG_NO_COLOR] Disable color output.
322323
--path=<value> [env: SHOPIFY_FLAG_PATH] The path to your app directory.

packages/cli/oclif.manifest.json

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -690,6 +690,15 @@
690690
"name": "config",
691691
"type": "option"
692692
},
693+
"json": {
694+
"allowNo": false,
695+
"char": "j",
696+
"description": "Output the result as JSON. Automatically disables color output.",
697+
"env": "SHOPIFY_FLAG_JSON",
698+
"hidden": false,
699+
"name": "json",
700+
"type": "boolean"
701+
},
693702
"no-color": {
694703
"allowNo": false,
695704
"description": "Disable color output.",

0 commit comments

Comments
 (0)