diff --git a/packages/php-wasm/progress/src/lib/emscripten-download-monitor.ts b/packages/php-wasm/progress/src/lib/emscripten-download-monitor.ts index bde572e8fa..c30a3c74f8 100644 --- a/packages/php-wasm/progress/src/lib/emscripten-download-monitor.ts +++ b/packages/php-wasm/progress/src/lib/emscripten-download-monitor.ts @@ -161,10 +161,12 @@ export function cloneStreamMonitorProgress( ); } - return new ReadableStream({ + let closed = false; + const monitoredStream = new ReadableStream({ async start(controller) { if (!stream) { controller.close(); + closed = true; return; } const reader = stream.getReader(); @@ -178,19 +180,29 @@ export function cloneStreamMonitorProgress( if (done) { notify(loaded, loaded); controller.close(); + closed = true; break; } else { notify(loaded, total); - controller.enqueue(value); + if (!closed) { + controller.enqueue(value); + } } } catch (e) { - logger.error({ e }); - controller.error(e); + debugger; + try { + console.log(controller); + logger.error({ e }); + console.dir(e); + controller.close(); + } catch (e) {} + // controller.error(e); break; } } }, }); + return monitoredStream; } export type DownloadProgressCallback = (progress: DownloadProgress) => void; diff --git a/packages/playground/blueprints/src/lib/resolve-remote-blueprint.ts b/packages/playground/blueprints/src/lib/resolve-remote-blueprint.ts index b9e4a141dd..6cbc8d70c9 100644 --- a/packages/playground/blueprints/src/lib/resolve-remote-blueprint.ts +++ b/packages/playground/blueprints/src/lib/resolve-remote-blueprint.ts @@ -19,7 +19,23 @@ export async function resolveRemoteBlueprint( credentials: 'omit', }); if (!response.ok) { - throw new Error(`Failed to fetch blueprint from ${url}`); + const statusText = response.statusText?.trim(); + const statusSummary = statusText + ? `${response.status} ${statusText}` + : `${response.status}`; + const error = new Error( + response.status + ? `Failed to fetch the Blueprint. The server responded with HTTP ${statusSummary}.` + : 'Failed to fetch the Blueprint.' + ) as Error & { + status?: number; + statusText?: string; + url?: string; + }; + error.status = response.status; + error.statusText = response.statusText; + error.url = url; + throw error; } const blueprintBytes = await response.arrayBuffer(); try { @@ -41,9 +57,11 @@ export async function resolveRemoteBlueprint( if (await looksLikeZipFile(blueprintBytes)) { return ZipFilesystem.fromArrayBuffer(blueprintBytes); } - throw new Error( + const error = new Error( `Blueprint file at ${url} is neither a valid JSON nor a ZIP file.` - ); + ) as Error & { url?: string }; + error.url = url; + throw error; } } diff --git a/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx b/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx index aea36cef8f..382e19c527 100644 --- a/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx +++ b/packages/playground/website/src/components/ensure-playground-site/ensure-playground-site-is-selected.tsx @@ -17,7 +17,10 @@ import { redirectTo } from '../../lib/state/url/router'; import { logger } from '@php-wasm/logger'; import { usePrevious } from '../../lib/hooks/use-previous'; import { modalSlugs } from '../layout'; -import { setActiveModal } from '../../lib/state/redux/slice-ui'; +import { + setActiveModal, + setActiveSiteError, +} from '../../lib/state/redux/slice-ui'; import { selectClientBySiteSlug } from '../../lib/state/redux/slice-clients'; import { randomSiteName } from '../../lib/state/redux/random-site-name'; @@ -158,8 +161,18 @@ async function createNewTemporarySite( const siteName = requestedSiteSlug ? deriveSiteNameFromSlug(requestedSiteSlug) : randomSiteName(); - const newSiteInfo = await dispatch( + const { site, blueprintResolutionFailed } = await dispatch( setTemporarySiteSpec(siteName, new URL(window.location.href)) ); - await dispatch(setActiveSite(newSiteInfo.slug)); + await dispatch(setActiveSite(site.slug)); + if (blueprintResolutionFailed) { + dispatch( + setActiveSiteError({ + error: 'blueprint-resolution-failed', + context: { + blueprintResolution: blueprintResolutionFailed, + }, + }) + ); + } } diff --git a/packages/playground/website/src/components/playground-viewport/index.tsx b/packages/playground/website/src/components/playground-viewport/index.tsx index 23be0e3951..9b5b47cad9 100644 --- a/packages/playground/website/src/components/playground-viewport/index.tsx +++ b/packages/playground/website/src/components/playground-viewport/index.tsx @@ -4,19 +4,24 @@ import css from './style.module.css'; import BrowserChrome from '../browser-chrome'; import { selectActiveSiteError, + selectActiveSiteErrorContext, useActiveSite, useAppDispatch, useAppSelector, } from '../../lib/state/redux/store'; import { removeClientInfo } from '../../lib/state/redux/slice-clients'; import { bootSiteClient } from '../../lib/state/redux/boot-site-client'; -import type { SiteError } from '../../lib/state/redux/slice-ui'; +import type { + ActiveSiteErrorContext, + SiteError, +} from '../../lib/state/redux/slice-ui'; import { Button, Spinner } from '@wordpress/components'; import { removeSite, selectSiteBySlug, selectSitesLoaded, selectTemporarySites, + DEFAULT_WELCOME_BLUEPRINT_URL, } from '../../lib/state/redux/slice-sites'; import classNames from 'classnames'; @@ -202,12 +207,17 @@ export const JustViewport = function JustViewport({ }, [siteSlug, iframeRef, runtimeConfigString]); const error = useAppSelector(selectActiveSiteError); + const errorContext = useAppSelector(selectActiveSiteErrorContext); if (error) { return (
+ WordPress Playground couldn't load the Blueprint below. The + file might be unavailable or invalid. +
+ {attemptedUrl ? ( + + ) : null} + {statusSummary ? ( +{statusSummary}
+ ) : null} + {shouldShowMessage ?{trimmedMessage}
: null} ++ Reload without a Blueprint to start with a blank WordPress + site. +
+ + > + ); + } if ( error === 'directory-handle-not-found-in-indexeddb' || error === 'directory-handle-permission-denied' diff --git a/packages/playground/website/src/components/playground-viewport/style.module.css b/packages/playground/website/src/components/playground-viewport/style.module.css index 4a40c29cf9..9e3d191067 100644 --- a/packages/playground/website/src/components/playground-viewport/style.module.css +++ b/packages/playground/website/src/components/playground-viewport/style.module.css @@ -37,6 +37,23 @@ } } +.blueprint-url-wrapper { + margin-bottom: 15px; + word-break: break-word; + overflow-wrap: anywhere; +} + +.blueprint-url { + display: inline-block; + word-break: break-word; + overflow-wrap: anywhere; +} + +.blueprintStatus { + font-weight: 600; + margin-bottom: 15px; +} + .hidden { display: none; } 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..810ff6d993 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 @@ -57,9 +57,9 @@ export function bootSiteClient( } catch (e) { logger.error(e); dispatch( - setActiveSiteError( - 'directory-handle-not-found-in-indexeddb' - ) + setActiveSiteError({ + error: 'directory-handle-not-found-in-indexeddb', + }) ); return; } @@ -82,13 +82,17 @@ export function bootSiteClient( logger.error(e); if (e instanceof DOMException && e.name === 'NotFoundError') { dispatch( - setActiveSiteError( - 'directory-handle-not-found-in-indexeddb' - ) + setActiveSiteError({ + error: 'directory-handle-not-found-in-indexeddb', + }) ); return; } - dispatch(setActiveSiteError('directory-handle-unknown-error')); + dispatch( + setActiveSiteError({ + error: 'directory-handle-unknown-error', + }) + ); return; } } @@ -197,9 +201,11 @@ export function bootSiteClient( (e as any).name === 'ArtifactExpiredError' || (e as any).originalErrorClassName === 'ArtifactExpiredError' ) { - dispatch(setActiveSiteError('github-artifact-expired')); + dispatch( + setActiveSiteError({ error: 'github-artifact-expired' }) + ); } else { - dispatch(setActiveSiteError('site-boot-failed')); + dispatch(setActiveSiteError({ error: 'site-boot-failed' })); dispatch(setActiveModal(modalSlugs.ERROR_REPORT)); } return; 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..023581036a 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 type { ActiveSiteErrorContext } from './slice-ui'; /** * The Site model used to represent a site within Playground. @@ -238,6 +239,18 @@ export function removeSite(slug: string) { * @param siteInfo The site info to add. * @returns */ +export const DEFAULT_WELCOME_BLUEPRINT_URL = + 'https://raw.githubusercontent.com/WordPress/blueprints/refs/heads/trunk/blueprints/welcome/blueprint.json'; + +export interface SetTemporarySiteSpecResult { + site: SiteInfo; + blueprintResolutionFailed?: ActiveSiteErrorContext['blueprintResolution']; +} + +type BlueprintResolutionFailure = NonNullable< + ActiveSiteErrorContext['blueprintResolution'] +>; + export function setTemporarySiteSpec( siteName: string, playgroundUrlWithQueryApiArgs: URL @@ -245,7 +258,7 @@ export function setTemporarySiteSpec( return async ( dispatch: PlaygroundDispatch, getState: () => PlaygroundReduxState - ) => { + ): Promise