Skip to content

Commit fca1cda

Browse files
committed
WIP: Support Blueprints v2 in the browser
1 parent 47d921f commit fca1cda

File tree

8 files changed

+383
-21
lines changed

8 files changed

+383
-21
lines changed

packages/php-wasm/universal/src/lib/php.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -326,7 +326,6 @@ export class PHP implements Disposable {
326326
return returnData;
327327
}
328328
}
329-
330329
return '';
331330
};
332331

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
import type { StreamedPHPResponse, UniversalPHP } from '@php-wasm/universal';
2+
import { phpVar } from '@php-wasm/util';
3+
import { getV2Runner } from '@wp-playground/blueprints';
4+
5+
export type PHPExceptionDetails = {
6+
exception: string;
7+
message: string;
8+
file: string;
9+
line: number;
10+
trace: string;
11+
};
12+
13+
export type BlueprintMessage =
14+
| { type: 'blueprint.target_resolved' }
15+
| { type: 'blueprint.progress'; progress: number; caption: string }
16+
| {
17+
type: 'blueprint.error';
18+
message: string;
19+
details?: PHPExceptionDetails;
20+
}
21+
| { type: 'blueprint.completion'; message: string };
22+
23+
export type BlueprintV2Declaration = string | Record<string, any> | undefined;
24+
export type ParsedBlueprintV2Declaration =
25+
| { type: 'inline-file'; contents: string }
26+
| { type: 'file-reference'; reference: string };
27+
28+
function parseBlueprintDeclaration(
29+
source: BlueprintV2Declaration | ParsedBlueprintV2Declaration
30+
): ParsedBlueprintV2Declaration {
31+
if (
32+
typeof source === 'object' &&
33+
'string' !== typeof (source as any) &&
34+
'type' in (source as any) &&
35+
['inline-file', 'file-reference'].includes((source as any).type)
36+
) {
37+
return source as ParsedBlueprintV2Declaration;
38+
}
39+
if (!source) {
40+
return { type: 'inline-file', contents: '{}' };
41+
}
42+
if (typeof source !== 'string') {
43+
return { type: 'inline-file', contents: JSON.stringify(source) };
44+
}
45+
try {
46+
JSON.parse(source);
47+
return { type: 'inline-file', contents: source };
48+
} catch {
49+
return { type: 'file-reference', reference: source };
50+
}
51+
}
52+
53+
interface RunV2Options {
54+
php: UniversalPHP;
55+
cliArgs?: string[];
56+
blueprint: BlueprintV2Declaration | ParsedBlueprintV2Declaration;
57+
blueprintOverrides?: {
58+
wordpressVersion?: string;
59+
additionalSteps?: any[];
60+
};
61+
onMessage?: (message: BlueprintMessage) => void | Promise<void>;
62+
}
63+
64+
export async function runBlueprintV2Web(
65+
options: RunV2Options
66+
): Promise<StreamedPHPResponse> {
67+
console.log('runBlueprintV2Web', options);
68+
69+
const php = options.php;
70+
const onMessage = options?.onMessage || (() => {});
71+
72+
const file = await getV2Runner();
73+
php.writeFile(
74+
'/tmp/blueprints.phar',
75+
new Uint8Array(await file.arrayBuffer())
76+
);
77+
78+
const parsedBlueprintDeclaration = parseBlueprintDeclaration(
79+
options.blueprint
80+
);
81+
let blueprintReference = '';
82+
switch (parsedBlueprintDeclaration.type) {
83+
case 'inline-file':
84+
php.writeFile(
85+
'/tmp/blueprint.json',
86+
parsedBlueprintDeclaration.contents
87+
);
88+
blueprintReference = '/tmp/blueprint.json';
89+
console.log(parsedBlueprintDeclaration.contents);
90+
break;
91+
case 'file-reference':
92+
blueprintReference = parsedBlueprintDeclaration.reference;
93+
break;
94+
}
95+
96+
const unbindMessageListener = await php.onMessage(async (message) => {
97+
try {
98+
const parsed =
99+
typeof message === 'string' ? JSON.parse(message) : message;
100+
if (!parsed) {
101+
return undefined;
102+
}
103+
if (parsed.type && parsed.type.startsWith('blueprint.')) {
104+
await onMessage(parsed);
105+
return 'handled!';
106+
}
107+
return undefined;
108+
} catch {
109+
// Ignore parse errors
110+
}
111+
return undefined;
112+
});
113+
114+
// @TODO: Careful with pre-existing sites!
115+
if (await php.fileExists('/wordpress')) {
116+
await php.rmdir('/wordpress', { recursive: true });
117+
await php.mkdir('/wordpress');
118+
}
119+
120+
await php?.writeFile(
121+
'/tmp/run-blueprints.php',
122+
`<?php
123+
124+
use WordPress\\CLI\\CLI;
125+
use WordPress\\Blueprints\\DataReference\\AbsoluteLocalPath;
126+
use WordPress\\Blueprints\\DataReference\\DataReference;
127+
use WordPress\\Blueprints\\DataReference\\ExecutionContextPath;
128+
use WordPress\\Blueprints\\Exception\\BlueprintExecutionException;
129+
use WordPress\\Blueprints\\Exception\\PermissionsException;
130+
use WordPress\\Blueprints\\Logger\\CLILogger;
131+
use WordPress\\Blueprints\\ProgressObserver;
132+
use WordPress\\Blueprints\\Runner;
133+
use WordPress\\Blueprints\\RunnerConfiguration;
134+
use WordPress\\Filesystem\\LocalFilesystem;
135+
136+
$argv = [];
137+
$GLOBALS['argv'] = $_SERVER['argv'] = array_merge([
138+
"/tmp/blueprints.phar"
139+
], []);
140+
141+
function playground_http_client_factory() {
142+
return new WordPress\\HttpClient\\Client([
143+
'transport' => 'sockets',
144+
]);
145+
}
146+
playground_add_filter('blueprint.http_client', 'playground_http_client_factory');
147+
148+
function playground_on_blueprint_target_resolved() {
149+
post_message_to_js(json_encode([
150+
'type' => 'blueprint.target_resolved',
151+
]));
152+
}
153+
playground_add_filter('blueprint.target_resolved', 'playground_on_blueprint_target_resolved');
154+
155+
// playground_add_filter('blueprint.resolved', 'playground_on_blueprint_resolved');
156+
function playground_on_blueprint_resolved($blueprint) {
157+
$additional_blueprint_steps = json_decode(${phpVar(
158+
JSON.stringify(options.blueprintOverrides?.additionalSteps || [])
159+
)}, true);
160+
if(count($additional_blueprint_steps) > 0) {
161+
$blueprint['additionalStepsAfterExecution'] = array_merge(
162+
$blueprint['additionalStepsAfterExecution'] ?? [],
163+
$additional_blueprint_steps
164+
);
165+
}
166+
167+
$wp_version_override = json_decode(${phpVar(
168+
JSON.stringify(options.blueprintOverrides?.wordpressVersion || null)
169+
)}, true);
170+
if($wp_version_override) {
171+
$blueprint['wordpressVersion'] = $wp_version_override;
172+
}
173+
return $blueprint;
174+
}
175+
176+
function playground_progress_reporter() {
177+
class PlaygroundProgressReporter implements ProgressReporter {
178+
179+
public function reportProgress(float $progress, string $caption): void {
180+
$this->writeJsonMessage([
181+
'type' => 'blueprint.progress',
182+
'progress' => round($progress, 2),
183+
'caption' => $caption
184+
]);
185+
}
186+
187+
public function reportError(string $message, ?Throwable $exception = null): void {
188+
$errorData = [
189+
'type' => 'blueprint.error',
190+
'message' => $message
191+
];
192+
193+
if ($exception) {
194+
$errorData['details'] = [
195+
'exception' => get_class($exception),
196+
'message' => $exception->getMessage(),
197+
'file' => $exception->getFile(),
198+
'line' => $exception->getLine(),
199+
'trace' => $exception->getTraceAsString()
200+
];
201+
}
202+
203+
$this->writeJsonMessage($errorData);
204+
}
205+
206+
public function reportCompletion(string $message): void {
207+
$this->writeJsonMessage([
208+
'type' => 'blueprint.completion',
209+
'message' => $message
210+
]);
211+
}
212+
213+
public function close(): void {}
214+
215+
private function writeJsonMessage(array $data): void {
216+
post_message_to_js(json_encode($data));
217+
}
218+
}
219+
return new PlaygroundProgressReporter();
220+
}
221+
playground_add_filter('blueprint.progress_reporter', 'playground_progress_reporter');
222+
post_message_to_js(json_encode([
223+
'type' => 'blueprint.target_resolved',
224+
]));
225+
226+
$argv = [];
227+
require( "/tmp/blueprints.phar" );
228+
229+
$config = new RunnerConfiguration();
230+
231+
// The first positional is the blueprint reference
232+
try {
233+
$blueprint_reference = ${phpVar(blueprintReference)};
234+
$config->setBlueprint( DataReference::create( $blueprint_reference, [
235+
AbsoluteLocalPath::class,
236+
ExecutionContextPath::class,
237+
] ) );
238+
} catch ( InvalidArgumentException $e ) {
239+
throw new InvalidArgumentException( sprintf( "Invalid Blueprint reference: %s. Hint: paths must start with ./ or /. URLs must start with http:// or https://.", $positionalArgs[0] ) );
240+
}
241+
242+
$config->setExecutionMode( Runner::EXECUTION_MODE_CREATE_NEW_SITE );
243+
244+
$targetSiteRoot = '/wordpress';
245+
246+
$absoluteTargetSiteRoot = realpath( $targetSiteRoot );
247+
if ( false === $absoluteTargetSiteRoot || ! is_dir( $absoluteTargetSiteRoot ) ) {
248+
throw new InvalidArgumentException( "The --site-path path does not exist: {$targetSiteRoot}" );
249+
}
250+
$config->setTargetSiteRoot( $absoluteTargetSiteRoot );
251+
$config->setTargetSiteUrl( ${phpVar(await php.absoluteUrl)} );
252+
253+
// Set database engine
254+
$config->setDatabaseEngine( 'sqlite' );
255+
$config->setDatabaseCredentials( [
256+
'path' => '/wordpress/wp-content/databases/.ht.sqlite',
257+
] );
258+
259+
$config->setLogger(
260+
new CLILogger( 'php://stdout', CLILogger::VERBOSITY_INFO )
261+
);
262+
$config->setProgressObserver( new ProgressObserver( function ( $progress, $caption ) use ( $progressReporter ) {
263+
$progressReporter->reportProgress( $progress, $caption );
264+
} ) );
265+
$runner = new Runner( $config );
266+
$runner->run();
267+
`
268+
);
269+
console.log({ blueprintReference });
270+
console.log('before runStream', {
271+
siteUrl: await php.absoluteUrl,
272+
});
273+
274+
const r = await php.run({
275+
scriptPath: '/tmp/run-blueprints.php',
276+
});
277+
unbindMessageListener();
278+
console.log('after runStream', r);
279+
return r;
280+
}

packages/playground/client/src/index.ts

Lines changed: 45 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@ export interface StartPlaygroundOptions {
4141
progressTracker?: ProgressTracker;
4242
disableProgressBar?: boolean;
4343
blueprint?: Blueprint;
44+
/**
45+
* Prefer experimental Blueprints v2 PHP runner instead of TypeScript steps
46+
*/
47+
experimentalBlueprintsV2Runner?: boolean;
4448
onBlueprintStepCompleted?: OnStepCompleted;
4549
/**
4650
* Called when the playground client is connected, but before the blueprint
@@ -114,6 +118,7 @@ export async function startPlaygroundWeb({
114118
corsProxy,
115119
shouldInstallWordPress,
116120
sqliteDriverVersion,
121+
experimentalBlueprintsV2Runner,
117122
}: StartPlaygroundOptions): Promise<PlaygroundClient> {
118123
assertLikelyCompatibleRemoteOrigin(remoteUrl);
119124
allowStorageAccessByUserActivation(iframe);
@@ -128,12 +133,19 @@ export async function startPlaygroundWeb({
128133
blueprint = {};
129134
}
130135

131-
const compiled = await compileBlueprint(blueprint, {
132-
progress: progressTracker.stage(0.5),
133-
onStepCompleted: onBlueprintStepCompleted,
134-
corsProxy,
135-
});
136-
136+
const compiled = experimentalBlueprintsV2Runner
137+
? await compileBlueprint(
138+
{},
139+
{
140+
progress: progressTracker.stage(0.5),
141+
corsProxy,
142+
}
143+
)
144+
: await compileBlueprint(blueprint, {
145+
progress: progressTracker.stage(0.5),
146+
onStepCompleted: onBlueprintStepCompleted,
147+
corsProxy,
148+
});
137149
await new Promise((resolve) => {
138150
iframe.src = remoteUrl;
139151
iframe.addEventListener('load', resolve, false);
@@ -167,8 +179,33 @@ export async function startPlaygroundWeb({
167179
collectPhpLogs(logger, playground);
168180
onClientConnected(playground);
169181

170-
if (onBeforeBlueprint) {
171-
await onBeforeBlueprint();
182+
// If the caller requested the v2 runner and provided a blueprint,
183+
// execute it via PHP before running any (empty) TS steps.
184+
if (experimentalBlueprintsV2Runner && blueprint) {
185+
const { runBlueprintV2Web } = await import(
186+
'./blueprints-v2/run-blueprint-v2-web'
187+
);
188+
await playground.setProgress({
189+
caption: 'Running Blueprint',
190+
isIndefinite: false,
191+
visible: true,
192+
progress: 0,
193+
});
194+
const streamed = await runBlueprintV2Web({
195+
php: playground,
196+
blueprint,
197+
onMessage: async (message) => {
198+
if ((message as any).type === 'blueprint.progress') {
199+
await playground.setProgress({
200+
caption: ((message as any).caption || 'Working').trim(),
201+
progress: (message as any).progress,
202+
isIndefinite: false,
203+
visible: true,
204+
});
205+
}
206+
},
207+
});
208+
await streamed.finished;
172209
}
173210

174211
await runBlueprintSteps(compiled, playground);

0 commit comments

Comments
 (0)