Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Skip Blueprint execution in the browser when persistent storage is used #1484

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions packages/playground/client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,12 @@ export interface StartPlaygroundOptions {
*
* @returns
*/
onBeforeBlueprint?: () => Promise<void>;
onBeforeBlueprint?: (
blueprint: CustomEvent<{
blueprint: Blueprint;
playground: PlaygroundClient;
}>
) => Promise<void>;
siteSlug?: string;
}

Expand Down Expand Up @@ -126,11 +131,19 @@ export async function startPlaygroundWeb({
collectPhpLogs(logger, playground);
onClientConnected(playground);

const event = new CustomEvent('beforeBlueprint', {
cancelable: true,
detail: {
playground,
blueprint,
},
});
if (onBeforeBlueprint) {
await onBeforeBlueprint();
await onBeforeBlueprint(event);
}
if (!event.defaultPrevented) {
await runBlueprintSteps(compiled, playground);
}

await runBlueprintSteps(compiled, playground);
progressTracker.finish();

return playground;
Expand Down
108 changes: 92 additions & 16 deletions packages/playground/website/src/lib/use-boot-playground.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { useEffect, useRef, useState } from 'react';
import { Blueprint, startPlaygroundWeb } from '@wp-playground/client';
import type { PlaygroundClient } from '@wp-playground/client';
import { Blueprint, login, startPlaygroundWeb } from '@wp-playground/client';
import type { LoginStep, PlaygroundClient } from '@wp-playground/client';
import { getRemoteUrl } from './config';
import { logger } from '@php-wasm/logger';
import { PlaygroundDispatch, setActiveModal } from './redux-store';
Expand Down Expand Up @@ -44,7 +44,6 @@ export function useBootPlayground({
if (storage) {
remoteUrl.searchParams.set('storage', storage);
}

let playgroundTmp: PlaygroundClient | undefined = undefined;
startPlaygroundWeb({
iframe,
Expand All @@ -57,21 +56,73 @@ export function useBootPlayground({
playgroundTmp = playground;
(window as any)['playground'] = playground;
},
async onBeforeBlueprint() {
async onBeforeBlueprint(event) {
const newDirectoryHandle = await directoryHandle;
if (!newDirectoryHandle) {
return;
if (newDirectoryHandle) {
await playgroundTmp!.bindOpfs({
opfs: newDirectoryHandle,
mountpoint: joinPaths(
await playgroundTmp!.documentRoot,
'wp-content',
'uploads',
'markdown'
),
initialSyncDirection: 'opfs-to-memfs',
});
}

// When WordPress is loaded from an external source, we may want to
// skip the Blueprint execution. Running the same Blueprint twice,
// installing the same plugins, overwriting the files etc. would
// just corrupt the WordPress instance.
// @TODO Find a better of checking wheter the WordPress site was just installed,
// or are we bringing in an existing site that, presumably, a Blueprint
// has already acted on.
//
// One way would be hooking into the beforeDatabaseSetup() hook in bootWordPress()
// in worker-thread.ts where the early bindOpfs() call is being made.
//
// Another would be passing the relevant DirectoryHandle from this function
// to startPlaygroundWeb and ever reasoning about it in the worker-thread.ts.
// This way we could do prior check to see whether we're bringing in an existing
// WordPress instance.
//
// Perhaps the best way would be to do both of the above – provide the DirectoryHandle,
// let the Boot protocol do its thing, and then, in the crucial moment, check whether
// a WordPress site is installed or not.
//
// This would allow us to:
// * Not distinguish between browser storage and native filesystem storage.
// * Not rely on file-based flags that can be accidentally deleted.
if (newDirectoryHandle || storage === 'browser') {
const client = event.detail.playground;
if (storage === 'browser') {
// When files are stored in OPFS, we can keep track of the
// Blueprint execution state by creating a flag file on
// site install.
const isLoadedFlagPath = joinPaths(
await client.documentRoot,
'.blueprint-loaded'
);
if (await client.fileExists(isLoadedFlagPath)) {
event.preventDefault();
} else {
await client.writeFile(isLoadedFlagPath, '');
}
} else {
// When the file come from a native filesystem directory,
// let's always prevent the Blueprint execution. The only
// way we can have that handle available at this stage is
// if the user booted a WordPress site using the UI modal,
// synchronized it to a local directory, and then reloaded
// the page. This means the Blueprint was already executed
// and we should skip it here.
event.preventDefault();
}
if (event.defaultPrevented) {
await skipBlueprint(client, event.detail.blueprint);
}
}
await playgroundTmp!.bindOpfs({
opfs: newDirectoryHandle,
mountpoint: joinPaths(
await playgroundTmp!.documentRoot,
'wp-content',
'uploads',
'markdown'
),
initialSyncDirection: 'opfs-to-memfs',
});
},
})
.catch((error) => {
Expand All @@ -89,3 +140,28 @@ export function useBootPlayground({

return { playground, url, iframeRef };
}

/**
* Uses the login step and the landingPage of the Blueprint
* when the rest of the execution is skipped.
* @TODO: Provide a canonical method of doing this without
* reasoning about an uncompiled Blueprint structure
* or having to maintain this function when new Blueprint
* schema or ideas are shipped.
*/
async function skipBlueprint(
playground: PlaygroundClient,
blueprint: Blueprint
) {
const loginStep =
blueprint.login ||
(blueprint.steps?.find(
(step) => typeof step === 'object' && step?.step === 'login'
) as LoginStep | undefined);
if (loginStep) {
await login(playground, typeof loginStep === 'object' ? loginStep : {});
}
if (blueprint?.landingPage) {
await playground.goTo(blueprint.landingPage);
}
}
Loading