@@ -9,7 +9,10 @@ import {
99import { logger } from '@php-wasm/logger' ;
1010import { Button , Icon , Notice } from '@wordpress/components' ;
1111import { download } from '@wordpress/icons' ;
12- import { resolveRuntimeConfiguration } from '@wp-playground/blueprints' ;
12+ import {
13+ resolveRuntimeConfiguration ,
14+ type BlueprintValidationResult ,
15+ } from '@wp-playground/blueprints' ;
1316import type { AsyncWritableFilesystem } from '@wp-playground/storage' ;
1417import { BlobWriter , Uint8ArrayReader , ZipWriter } from '@zip.js/zip.js' ;
1518import classNames from 'classnames' ;
@@ -32,6 +35,7 @@ import {
3235 getStringNodeAtPosition ,
3336 jsonSchemaCompletion ,
3437} from './json-schema-editor/jsonSchemaCompletion' ;
38+ import { createBlueprintLinter } from './json-schema-editor/blueprint-linter' ;
3539import {
3640 inferLanguageFromBlueprint ,
3741 type SupportedLanguage ,
@@ -45,10 +49,42 @@ import { sitesSlice } from '../../lib/state/redux/slice-sites';
4549import { useAppDispatch } from '../../lib/state/redux/store' ;
4650import styles from '../site-manager/site-file-browser/style.module.css' ;
4751import hideRootStyles from './hide-root.module.css' ;
52+ import validationStyles from './validation-panel.module.css' ;
4853import type { EventedFilesystem } from '@wp-playground/storage' ;
4954
5055const BLUEPRINT_JSON_PATH = '/blueprint.json' ;
5156
57+ /**
58+ * Format a validation error into a human-readable message for the error panel
59+ */
60+ function formatValidationError ( error : {
61+ keyword : string ;
62+ message ?: string ;
63+ params ?: Record < string , unknown > ;
64+ instancePath : string ;
65+ } ) : string {
66+ // Provide better messages based on error type
67+ if ( error . keyword === 'additionalProperties' && error . params ) {
68+ const prop = error . params . additionalProperty ;
69+ return `Unknown property "${ prop } "` ;
70+ }
71+ if ( error . keyword === 'required' && error . params ) {
72+ const prop = error . params . missingProperty ;
73+ return `Missing required property "${ prop } "` ;
74+ }
75+ if ( error . keyword === 'enum' && error . params ) {
76+ const allowed = error . params . allowedValues ;
77+ if ( Array . isArray ( allowed ) ) {
78+ return `Value must be one of: ${ allowed . join ( ', ' ) } ` ;
79+ }
80+ }
81+ if ( error . keyword === 'type' && error . params ) {
82+ const expected = error . params . type ;
83+ return `Expected ${ expected } ` ;
84+ }
85+ return error . message || 'Validation error' ;
86+ }
87+
5288interface StringEditorState {
5389 isOpen : boolean ;
5490 initialValue : string ;
@@ -248,6 +284,8 @@ export const BlueprintBundleEditor = forwardRef<
248284 > ( null ) ;
249285 const [ displayPath , setDisplayPath ] = useState < string | null > ( null ) ;
250286 const [ isRecreating , setIsRecreating ] = useState ( false ) ;
287+ const [ validationResult , setValidationResult ] =
288+ useState < BlueprintValidationResult | null > ( null ) ;
251289 const [ stringEditorState , setStringEditorState ] =
252290 useState < StringEditorState > ( {
253291 isOpen : false ,
@@ -467,13 +505,21 @@ export const BlueprintBundleEditor = forwardRef<
467505 [ ]
468506 ) ;
469507
508+ const handleValidationChange = useCallback (
509+ ( result : BlueprintValidationResult | null ) => {
510+ setValidationResult ( result ) ;
511+ } ,
512+ [ ]
513+ ) ;
514+
470515 const blueprintSchemaExtensions = useMemo (
471516 ( ) => [
472517 autocompletion ( {
473518 override : [ jsonSchemaCompletion ] ,
474519 activateOnTyping : true ,
475520 closeOnBlur : false ,
476521 } ) ,
522+ createBlueprintLinter ( handleValidationChange ) ,
477523 // Capture the EditorView reference for string editor operations
478524 EditorView . updateListener . of ( ( update ) => {
479525 cmViewRef . current = update . view ;
@@ -489,9 +535,12 @@ export const BlueprintBundleEditor = forwardRef<
489535 // String editor toolbar tooltip
490536 createStringEditorTooltip ( openStringEditor ) ,
491537 ] ,
492- [ openStringEditor ]
538+ [ handleValidationChange , openStringEditor ]
493539 ) ;
494540
541+ const hasValidationErrors =
542+ validationResult !== null && ! validationResult . valid ;
543+
495544 const handleDownloadBundle = useCallback ( async ( ) => {
496545 try {
497546 const zipWriter = new ZipWriter ( new BlobWriter ( 'application/zip' ) ) ;
@@ -542,7 +591,7 @@ export const BlueprintBundleEditor = forwardRef<
542591 [ handleDownloadBundle , filesystem , handleRecreateFromBlueprint ]
543592 ) ;
544593
545- const disableRunButton = isRecreating || ! site ;
594+ const disableRunButton = isRecreating || ! site || hasValidationErrors ;
546595 return (
547596 < >
548597 < div className = { classNames ( styles . container , className ) } >
@@ -612,10 +661,21 @@ export const BlueprintBundleEditor = forwardRef<
612661 { ! readOnly && (
613662 < Button
614663 variant = "primary"
615- className = { styles . editorToolbarButton }
664+ className = { classNames (
665+ styles . editorToolbarButton ,
666+ {
667+ [ validationStyles . runButtonDisabled ] :
668+ hasValidationErrors ,
669+ }
670+ ) }
616671 onClick = { handleRecreateFromBlueprint }
617672 isBusy = { isRecreating }
618673 disabled = { disableRunButton }
674+ title = {
675+ hasValidationErrors
676+ ? 'Fix validation errors before running'
677+ : undefined
678+ }
619679 >
620680 < PlayIcon
621681 className = {
@@ -640,19 +700,94 @@ export const BlueprintBundleEditor = forwardRef<
640700 { messageContent }
641701 </ div >
642702 ) : (
643- < CodeEditor
644- ref = { editorRef }
645- code = { code }
646- onChange = { handleCodeChange }
647- currentPath = { currentPath }
648- className = { styles . editor }
649- readOnly = { readOnly }
650- additionalExtensions = {
651- currentPath === BLUEPRINT_JSON_PATH
652- ? blueprintSchemaExtensions
653- : undefined
654- }
655- />
703+ < >
704+ < CodeEditor
705+ ref = { editorRef }
706+ code = { code }
707+ onChange = { handleCodeChange }
708+ currentPath = { currentPath }
709+ className = { styles . editor }
710+ readOnly = { readOnly }
711+ additionalExtensions = {
712+ currentPath === BLUEPRINT_JSON_PATH
713+ ? blueprintSchemaExtensions
714+ : undefined
715+ }
716+ />
717+ { currentPath === BLUEPRINT_JSON_PATH &&
718+ hasValidationErrors &&
719+ ! validationResult . valid && (
720+ < div
721+ className = {
722+ validationStyles . validationPanel
723+ }
724+ >
725+ < div
726+ className = {
727+ validationStyles . validationHeader
728+ }
729+ >
730+ < span
731+ className = {
732+ validationStyles . validationIcon
733+ }
734+ >
735+ < svg
736+ viewBox = "0 0 20 20"
737+ fill = "currentColor"
738+ >
739+ < path
740+ fillRule = "evenodd"
741+ d = "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z"
742+ clipRule = "evenodd"
743+ />
744+ </ svg >
745+ </ span >
746+ { validationResult . errors
747+ . length === 1
748+ ? '1 validation error'
749+ : `${ validationResult . errors . length } validation errors` }
750+ </ div >
751+ < ul
752+ className = {
753+ validationStyles . validationErrors
754+ }
755+ >
756+ { validationResult . errors . map (
757+ ( error , index ) => (
758+ < li
759+ key = { index }
760+ className = {
761+ validationStyles . validationError
762+ }
763+ >
764+ { error . instancePath && (
765+ < span
766+ className = {
767+ validationStyles . errorPath
768+ }
769+ >
770+ {
771+ error . instancePath
772+ }
773+ </ span >
774+ ) }
775+ < span
776+ className = {
777+ validationStyles . errorMessage
778+ }
779+ >
780+ { formatValidationError (
781+ error
782+ ) }
783+ </ span >
784+ </ li >
785+ )
786+ ) }
787+ </ ul >
788+ </ div >
789+ ) }
790+ </ >
656791 )
657792 ) : (
658793 < div className = { styles . placeholder } >
0 commit comments