From 6c8d2ed0ad7685298a43ae66d8ec7f194790cd62 Mon Sep 17 00:00:00 2001 From: Ryan Bahan Date: Mon, 30 Mar 2026 12:49:26 -0600 Subject: [PATCH] Add structured {valid, issues} JSON contract for app config validate Co-Authored-By: Claude Opus 4.6 (1M context) --- .../app/src/cli/services/validate.test.ts | 29 +++++++++++++++---- packages/app/src/cli/services/validate.ts | 6 ++-- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/packages/app/src/cli/services/validate.test.ts b/packages/app/src/cli/services/validate.test.ts index 3f390aa032..f6c1644664 100644 --- a/packages/app/src/cli/services/validate.test.ts +++ b/packages/app/src/cli/services/validate.test.ts @@ -39,7 +39,7 @@ describe('validateApp', () => { test('outputs json success when --json is enabled and there are no errors', async () => { const app = testAppLinked() await validateApp(app, {json: true}) - expect(outputResult).toHaveBeenCalledWith(JSON.stringify({valid: true, errors: []}, null, 2)) + expect(outputResult).toHaveBeenCalledWith(JSON.stringify({valid: true, issues: []}, null, 2)) expect(renderSuccess).not.toHaveBeenCalled() expect(renderError).not.toHaveBeenCalled() }) @@ -60,7 +60,7 @@ describe('validateApp', () => { expect(outputResult).not.toHaveBeenCalled() }) - test('outputs json errors and throws when --json is enabled and there are validation errors', async () => { + test('outputs structured json issues when --json is enabled and there are validation errors', async () => { const errors = new AppErrors() errors.addError({file: '/path/to/shopify.app.toml', message: 'client_id is required'}) errors.addError({file: '/path/to/extensions/my-ext/shopify.extension.toml', message: 'invalid type "unknown"'}) @@ -69,7 +69,17 @@ describe('validateApp', () => { await expect(validateApp(app, {json: true})).rejects.toThrow(AbortSilentError) expect(outputResult).toHaveBeenCalledWith( - JSON.stringify({valid: false, errors: ['client_id is required', 'invalid type "unknown"']}, null, 2), + JSON.stringify( + { + valid: false, + issues: [ + {file: '/path/to/shopify.app.toml', message: 'client_id is required'}, + {file: '/path/to/extensions/my-ext/shopify.extension.toml', message: 'invalid type "unknown"'}, + ], + }, + null, + 2, + ), ) expect(renderError).not.toHaveBeenCalled() expect(renderSuccess).not.toHaveBeenCalled() @@ -84,13 +94,22 @@ describe('validateApp', () => { expect(outputResult).not.toHaveBeenCalled() }) - test('formats structured errors with paths for JSON output', async () => { + test('includes path and code in structured json issues', async () => { const errors = new AppErrors() errors.addError({file: '/path/to/shopify.app.toml', path: ['name'], message: 'Required', code: 'invalid_type'}) const app = testAppLinked() app.errors = errors await expect(validateApp(app, {json: true})).rejects.toThrow(AbortSilentError) - expect(outputResult).toHaveBeenCalledWith(JSON.stringify({valid: false, errors: ['[name]: Required']}, null, 2)) + expect(outputResult).toHaveBeenCalledWith( + JSON.stringify( + { + valid: false, + issues: [{file: '/path/to/shopify.app.toml', message: 'Required', path: ['name'], code: 'invalid_type'}], + }, + null, + 2, + ), + ) }) }) diff --git a/packages/app/src/cli/services/validate.ts b/packages/app/src/cli/services/validate.ts index ddca3fe685..00b634c90a 100644 --- a/packages/app/src/cli/services/validate.ts +++ b/packages/app/src/cli/services/validate.ts @@ -13,7 +13,7 @@ export async function validateApp(app: AppLinkedInterface, options: ValidateAppO if (!appErrors || appErrors.isEmpty()) { if (options.json) { - outputResult(JSON.stringify({valid: true, errors: []}, null, 2)) + outputResult(JSON.stringify({valid: true, issues: []}, null, 2)) return } @@ -24,8 +24,8 @@ export async function validateApp(app: AppLinkedInterface, options: ValidateAppO const errors = appErrors.getErrors() if (options.json) { - const errorMessages = errors.map(formatConfigurationError) - outputResult(JSON.stringify({valid: false, errors: errorMessages}, null, 2)) + const issues = errors.map(({file, message, path, code}) => ({file, message, path, code})) + outputResult(JSON.stringify({valid: false, issues}, null, 2)) throw new AbortSilentError() }