@@ -2,9 +2,13 @@ import {appFlags} from '../../../flags.js'
22import { validateApp } from '../../../services/validate.js'
33import AppLinkedCommand , { AppLinkedCommandOutput } from '../../../utilities/app-linked-command.js'
44import { linkedAppContext } from '../../../services/app-context.js'
5+ import { selectActiveConfig } from '../../../models/project/active-config.js'
6+ import { errorsForConfig } from '../../../models/project/config-selection.js'
7+ import { Project } from '../../../models/project/project.js'
58import { globalFlags , jsonFlag } from '@shopify/cli-kit/node/cli'
69import { AbortError , AbortSilentError } from '@shopify/cli-kit/node/error'
710import { outputResult , stringifyMessage , unstyled } from '@shopify/cli-kit/node/output'
11+ import { renderError } from '@shopify/cli-kit/node/ui'
812
913export default class Validate extends AppLinkedCommand {
1014 static summary = 'Validate your app configuration and extensions.'
@@ -22,6 +26,47 @@ export default class Validate extends AppLinkedCommand {
2226 public async run ( ) : Promise < AppLinkedCommandOutput > {
2327 const { flags} = await this . parse ( Validate )
2428
29+ // Stage 1: Load project
30+ let project : Project
31+ try {
32+ project = await Project . load ( flags . path )
33+ } catch ( err ) {
34+ if ( err instanceof AbortError && flags . json ) {
35+ const message = unstyled ( stringifyMessage ( err . message ) ) . trim ( )
36+ outputResult ( JSON . stringify ( { valid : false , issues : [ { message} ] } , null , 2 ) )
37+ throw new AbortSilentError ( )
38+ }
39+ throw err
40+ }
41+
42+ // Stage 2: Select active config and check for TOML parse errors scoped to it
43+ let activeConfig
44+ try {
45+ activeConfig = await selectActiveConfig ( project , flags . config )
46+ } catch ( err ) {
47+ if ( err instanceof AbortError && flags . json ) {
48+ const message = unstyled ( stringifyMessage ( err . message ) ) . trim ( )
49+ outputResult ( JSON . stringify ( { valid : false , issues : [ { message} ] } , null , 2 ) )
50+ throw new AbortSilentError ( )
51+ }
52+ throw err
53+ }
54+
55+ const configErrors = errorsForConfig ( project , activeConfig . file )
56+ if ( configErrors . length > 0 ) {
57+ const issues = configErrors . map ( ( err ) => ( { file : err . path , message : err . message } ) )
58+ if ( flags . json ) {
59+ outputResult ( JSON . stringify ( { valid : false , issues} , null , 2 ) )
60+ throw new AbortSilentError ( )
61+ }
62+ renderError ( {
63+ headline : 'Validation errors found.' ,
64+ body : issues . map ( ( issue ) => `• ${ issue . message } ` ) . join ( '\n' ) ,
65+ } )
66+ throw new AbortSilentError ( )
67+ }
68+
69+ // Stage 3: Load app (link + remote fetch + schema validation)
2570 let app
2671 try {
2772 const context = await linkedAppContext ( {
@@ -33,9 +78,12 @@ export default class Validate extends AppLinkedCommand {
3378 } )
3479 app = context . app
3580 } catch ( err ) {
36- if ( err instanceof AbortError && flags . json ) {
37- const message = unstyled ( stringifyMessage ( err . message ) ) . trim ( )
38- outputResult ( JSON . stringify ( { valid : false , errors : [ { message} ] } , null , 2 ) )
81+ // Only catch config validation errors for JSON output. Auth/linking/remote
82+ // failures should propagate normally — they aren't validation results.
83+ const message = err instanceof AbortError ? unstyled ( stringifyMessage ( err . message ) ) . trim ( ) : ''
84+ const isValidationError = message . startsWith ( 'Validation errors in ' )
85+ if ( isValidationError && flags . json ) {
86+ outputResult ( JSON . stringify ( { valid : false , issues : [ { message} ] } , null , 2 ) )
3987 throw new AbortSilentError ( )
4088 }
4189 throw err
0 commit comments