diff --git a/packages/playground/blueprints/src/lib/v1/compile.ts b/packages/playground/blueprints/src/lib/v1/compile.ts index b2b3b7bb89..ace3064fc9 100644 --- a/packages/playground/blueprints/src/lib/v1/compile.ts +++ b/packages/playground/blueprints/src/lib/v1/compile.ts @@ -297,10 +297,93 @@ function compileBlueprintJson( const { valid, errors } = validateBlueprint(blueprint); if (!valid) { + // Format all validation errors with context + const errorMessages = errors! + .map((err, index) => { + const path = err.instancePath || '/'; + let message = err.message || 'validation failed'; + + // For "additional properties" errors, highlight the actual problematic key + let highlightedSnippet = ''; + if (message.includes('must NOT have additional properties')) { + // Extract the property name from the error params + const additionalProperty = (err.params as any) + ?.additionalProperty; + if (additionalProperty) { + message = `has unexpected property "${additionalProperty}"`; + + // Try to show the offending key highlighted + try { + const pathParts = path.split('/').filter(Boolean); + let currentValue: any = blueprint; + for (const part of pathParts) { + if ( + currentValue && + typeof currentValue === 'object' + ) { + currentValue = currentValue[part]; + } + } + + if ( + currentValue && + typeof currentValue === 'object' + ) { + const offendingValue = + currentValue[additionalProperty]; + const valueStr = JSON.stringify(offendingValue); + highlightedSnippet = `\n "${additionalProperty}": ${valueStr}\n ${'^'.repeat( + additionalProperty.length + 2 + )} This property is not recognized`; + } + } catch { + // If we can't extract context, that's okay + } + } + } else { + // For other errors, try to extract the offending value + try { + const pathParts = path.split('/').filter(Boolean); + let currentValue: any = blueprint; + for (const part of pathParts) { + if ( + currentValue && + typeof currentValue === 'object' + ) { + currentValue = currentValue[part]; + } + } + if (currentValue !== undefined) { + const valueStr = JSON.stringify( + currentValue, + null, + 2 + ); + // Limit snippet length + const snippet = + valueStr.length > 200 + ? valueStr.substring(0, 200) + '...' + : valueStr; + highlightedSnippet = `\n Value: ${snippet}`; + } + } catch { + // If we can't extract context, that's okay + } + } + + return `${ + index + 1 + }. At path "${path}": ${message}${highlightedSnippet}`; + }) + .join('\n\n'); + const e = new Error( - `Invalid blueprint: ${errors![0].message} at ${ - errors![0].instancePath - }` + `Invalid Blueprint: The Blueprint does not conform to the schema.\n\n` + + `Found ${ + errors!.length + } validation error(s):\n\n${errorMessages}\n\n` + + `Please review your Blueprint and fix these issues. ` + + `Learn more about the Blueprint format: https://wordpress.github.io/wordpress-playground/blueprints/data-format` ); // Attach Ajv output to the thrown object for easier debugging (e as any).errors = errors; diff --git a/packages/playground/blueprints/src/lib/v1/resources.ts b/packages/playground/blueprints/src/lib/v1/resources.ts index 675ca531f5..f25896b698 100644 --- a/packages/playground/blueprints/src/lib/v1/resources.ts +++ b/packages/playground/blueprints/src/lib/v1/resources.ts @@ -193,7 +193,16 @@ export abstract class Resource { case 'bundled': if (!streamBundledFile) { throw new Error( - 'Filesystem is required for blueprint resources' + 'Blueprint resource of type "bundled" requires a filesystem.\n\n' + + 'This Blueprint refers to files that should be bundled with it (like images, plugins, or themes), ' + + 'but the filesystem needed to access these files is not available. This usually happens when:\n\n' + + "1. You're trying to load a Blueprint as a standalone JSON file that was meant to be part of a bundle\n" + + '2. The Blueprint was not packaged correctly as a blueprint.zip file\n\n' + + 'To fix this:\n' + + "• If you're loading from a URL, make sure all referenced files are accessible relative to the Blueprint file\n" + + "• If you're using a blueprint.zip file, ensure it contains all the files referenced in the Blueprint\n" + + '• Check that the "resource": "bundled" references in your Blueprint match actual files in your bundle\n\n' + + 'Learn more about Blueprint resources: https://wordpress.github.io/wordpress-playground/blueprints/data-format#resources' ); } resource = new BundledResource( diff --git a/packages/playground/website/src/components/playground-viewport/index.tsx b/packages/playground/website/src/components/playground-viewport/index.tsx index 8d3eabca10..26f602c340 100644 --- a/packages/playground/website/src/components/playground-viewport/index.tsx +++ b/packages/playground/website/src/components/playground-viewport/index.tsx @@ -319,6 +319,202 @@ function SiteErrorMessage({ ); } + if (error === 'blueprint-fetch-failed') { + const errorDetails = (window as any).__playgroundBlueprintError; + const errorMessage = + errorDetails instanceof Error + ? errorDetails.message + : String(errorDetails || 'Unknown error'); + + return ( + <> +

Failed to load Blueprint

+

+ The Blueprint could not be downloaded or loaded. This + usually happens when: +

+ +
+ + Error details + +
+						{errorMessage}
+					
+
+

+ + Learn more about troubleshooting Blueprints + +

+ + + ); + } + + if (error === 'blueprint-filesystem-required') { + const errorDetails = (window as any).__playgroundBlueprintError; + const errorMessage = + errorDetails instanceof Error + ? errorDetails.message + : String(errorDetails || 'Unknown error'); + + return ( + <> +

Blueprint Resource Error

+

+ This Blueprint refers to files that should be bundled with + it (like images, plugins, or themes), but the filesystem + needed to access these files is not available. +

+

+ Common causes: +

+ +
+ + Error details + +
+						{errorMessage}
+					
+
+

+ + Learn more about Blueprint resources + +

+ + + ); + } + + if (error === 'blueprint-validation-failed') { + const errorDetails = (window as any).__playgroundBlueprintError; + const errorMessage = + errorDetails instanceof Error + ? errorDetails.message + : String(errorDetails || 'Unknown error'); + + return ( + <> +

Invalid Blueprint

+

+ The Blueprint does not conform to the required schema. + Please review the validation errors below and fix your + Blueprint. +

+
+ + Validation errors + +
+						{errorMessage}
+					
+
+

+ + Learn more about the Blueprint format + +

+ + + ); + } + return ( <>

Something went wrong

diff --git a/packages/playground/website/src/lib/state/redux/boot-site-client.ts b/packages/playground/website/src/lib/state/redux/boot-site-client.ts index 7f5e87e613..1804f584b6 100644 --- a/packages/playground/website/src/lib/state/redux/boot-site-client.ts +++ b/packages/playground/website/src/lib/state/redux/boot-site-client.ts @@ -193,11 +193,27 @@ export function bootSiteClient( } } catch (e) { logger.error(e); + + // Store the error details for display + (window as any).__playgroundBlueprintError = e; + if ( (e as any).name === 'ArtifactExpiredError' || (e as any).originalErrorClassName === 'ArtifactExpiredError' ) { dispatch(setActiveSiteError('github-artifact-expired')); + } else if ( + e instanceof Error && + e.message.includes( + 'Blueprint resource of type "bundled" requires a filesystem' + ) + ) { + dispatch(setActiveSiteError('blueprint-filesystem-required')); + } else if ( + e instanceof Error && + e.message.startsWith('Invalid Blueprint:') + ) { + dispatch(setActiveSiteError('blueprint-validation-failed')); } else { dispatch(setActiveSiteError('site-boot-failed')); dispatch(setActiveModal(modalSlugs.ERROR_REPORT)); diff --git a/packages/playground/website/src/lib/state/redux/slice-sites.ts b/packages/playground/website/src/lib/state/redux/slice-sites.ts index b776f5d137..747fcc488e 100644 --- a/packages/playground/website/src/lib/state/redux/slice-sites.ts +++ b/packages/playground/website/src/lib/state/redux/slice-sites.ts @@ -20,6 +20,7 @@ import { applyQueryOverrides, } from '../url/resolve-blueprint-from-url'; import { logger } from '@php-wasm/logger'; +import { setActiveSiteError } from './slice-ui'; /** * The Site model used to represent a site within Playground. @@ -286,15 +287,47 @@ export function setTemporarySiteSpec( ); } catch (e) { logger.error( - 'Error resolving blueprint, fallink back to a blank blueprint.', + 'Error resolving blueprint: Blueprint could not be downloaded or loaded.', e ); - // TODO: This is a hack – we are just abusing a URL-oriented - // function to create a completely blank Blueprint. Let's fix this by - // making default creation first-class. - resolvedBlueprint = await resolveBlueprintFromURL( - new URL('https://w.org') - ); + + // Store the error details for the error modal + (window as any).__playgroundBlueprintError = e; + + // Show error to the user - create a minimal site to display the error + const errorSite: SiteInfo = { + slug: deriveSlugFromSiteName(siteName), + originalUrlParams: newSiteUrlParams, + metadata: { + name: siteName, + id: crypto.randomUUID(), + whenCreated: Date.now(), + storage: 'none' as const, + originalBlueprint: {}, + originalBlueprintSource: { + type: 'url', + url: playgroundUrlWithQueryApiArgs.toString(), + }, + runtimeConfiguration: { + phpVersion: '8.0', + wpVersion: 'latest', + intl: false, + networking: true, + extraLibraries: [], + constants: {}, + }, + }, + }; + + dispatch(sitesSlice.actions.addSite(errorSite)); + dispatch(sitesSlice.actions.setFirstTemporarySiteCreated()); + + // Set the error state for this site + setTimeout(() => { + dispatch(setActiveSiteError('blueprint-fetch-failed')); + }, 0); + + return errorSite; } const reflection = await BlueprintReflection.create( diff --git a/packages/playground/website/src/lib/state/redux/slice-ui.ts b/packages/playground/website/src/lib/state/redux/slice-ui.ts index 0e4e23419a..3154cf0682 100644 --- a/packages/playground/website/src/lib/state/redux/slice-ui.ts +++ b/packages/playground/website/src/lib/state/redux/slice-ui.ts @@ -8,7 +8,10 @@ export type SiteError = | 'directory-handle-unknown-error' // @TODO: Improve name? | 'site-boot-failed' - | 'github-artifact-expired'; + | 'github-artifact-expired' + | 'blueprint-fetch-failed' + | 'blueprint-filesystem-required' + | 'blueprint-validation-failed'; export type SiteManagerSection = 'sidebar' | 'site-details' | 'blueprints'; export interface UIState {