Skip to content
Closed
Show file tree
Hide file tree
Changes from 16 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
a2f1f3d
WIP: Support Blueprints v2 in the browser
adamziel Sep 2, 2025
4a92c02
Configure the Blueprints runner via env variables
adamziel Sep 2, 2025
f3fedc8
Run Blueprints.phar via php.cli() sapi
adamziel Sep 3, 2025
666981f
Run Blueprints.phar via php.cli() sapi, rotate the spawn handler
adamziel Sep 3, 2025
2650062
Merge trunk
adamziel Sep 3, 2025
94388e6
Merge branch 'trunk' into support-blueprints-v2-in-the-browser
adamziel Sep 3, 2025
0281f66
Merge trunk
adamziel Sep 3, 2025
4ae58e7
Merge branch 'trunk' into support-blueprints-v2-in-the-browser
adamziel Sep 8, 2025
c66b498
Lint
adamziel Sep 8, 2025
78200dd
Lint
adamziel Sep 8, 2025
9ab367a
separate Blueprints v2 execution from Blueprints v1
adamziel Sep 8, 2025
6a227e0
Fix wasm crash by removing fd_close from JSPI_IMPORTS in the browser
adamziel Sep 8, 2025
a99da43
Run Blueprints v2 in the browser
adamziel Sep 9, 2025
4c5a5c2
Merge branch 'trunk' into support-blueprints-v2-in-the-browser
adamziel Sep 9, 2025
b43af5d
Lint
adamziel Sep 9, 2025
c50aa4b
Lint
adamziel Sep 9, 2025
43325aa
don't run compileBlueprint for v2 in createSiteMetadata
adamziel Sep 11, 2025
74597c9
Merge branch 'trunk' into support-blueprints-v2-in-the-browser
adamziel Sep 11, 2025
9d5e37d
Apply V2 URL overrides to V2 Blueprints
adamziel Sep 11, 2025
df8a8be
simplify createSiteMetadata
adamziel Sep 11, 2025
550186f
Add missing TS types for Blueprints v2
adamziel Sep 11, 2025
18c4552
Merge branch 'trunk' into support-blueprints-v2-in-the-browser
adamziel Sep 12, 2025
49179e4
Rewire state management
adamziel Sep 12, 2025
da16236
Rewire state management
adamziel Sep 12, 2025
4a44a81
Merge branch 'trunk' into support-blueprints-v2-in-the-browser
adamziel Sep 12, 2025
449b2f0
Lint, use reflections to analyze Blueprints
adamziel Sep 12, 2025
33fa5b1
Adjust Blueprint schema
adamziel Sep 12, 2025
2b8a062
Get rid of another compileBlueprint() call in favor of reflections
adamziel Sep 12, 2025
b63900f
Draft php command
adamziel Sep 17, 2025
2f4d66f
Merge branch 'trunk' into support-blueprints-v2-in-the-browser
adamziel Sep 18, 2025
6991ee4
Restore php.wasm from trunk
adamziel Sep 18, 2025
0428468
Restore files from trunk
adamziel Sep 18, 2025
f3648e1
Draft php command
adamziel Sep 17, 2025
b66d620
Merge branch 'trunk' into support-blueprints-v2-in-the-browser
adamziel Sep 18, 2025
4a734e7
Restore trunk php builds
adamziel Sep 18, 2025
d1b25a0
Restore trunk changelog
adamziel Sep 18, 2025
8e25010
Revert "Draft php command"
adamziel Sep 18, 2025
1f3b654
Restore files from trunk
adamziel Sep 18, 2025
7682a2b
Merge branch 'trunk' into support-blueprints-v2-in-the-browser
adamziel Sep 18, 2025
0dad295
resolve conflicts
adamziel Sep 18, 2025
0c091f5
resolve conflicts
adamziel Sep 18, 2025
f465e26
export type
adamziel Sep 18, 2025
f057db1
Restore files from trunk
adamziel Sep 18, 2025
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
4 changes: 2 additions & 2 deletions packages/php-wasm/compile/libopenssl/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,12 @@ RUN set -euxo pipefail && \
RUN source /root/emsdk/emsdk_env.sh && \
cd openssl-$OPENSSL_VERSION && \
sed -i 's|^CROSS_COMPILE.*$|CROSS_COMPILE=|g' Makefile && \
export JSPI_FLAGS=$(if [ "$JSPI" = "1" ]; then echo "-sSUPPORT_LONGJMP=wasm -fwasm-exceptions"; else echo ""; fi) && \
export JSPI_FLAGS=$(if [ "$JSPI" = "1" ]; then echo "-sJSPI=1 -sSUPPORT_LONGJMP=wasm -fwasm-exceptions -sJSPI_IMPORTS=close,fd_close,wasm_close -sJSPI_EXPORTS=close,fd_close,wasm_close"; else echo ""; fi) && \
EMCC_FLAGS=" -sSIDE_MODULE $JSPI_FLAGS" EMCC_SKIP="-lz" emmake make -j 12 build_generated libssl.a libcrypto.a;

RUN source /root/emsdk/emsdk_env.sh && \
cd openssl-$OPENSSL_VERSION && \
export JSPI_FLAGS=$(if [ "$JSPI" = "1" ]; then echo "-sSUPPORT_LONGJMP=wasm -fwasm-exceptions"; else echo ""; fi) && \
export JSPI_FLAGS=$(if [ "$JSPI" = "1" ]; then echo "-sJSPI=1 -sJSPI_IMPORTS=close -sJSPI_EXPORTS=close,fd_close,wasm_close -sSUPPORT_LONGJMP=wasm -fwasm-exceptions"; else echo ""; fi) && \
cp -RL include/openssl /root/lib/include && \
cp libcrypto.a libssl.a /root/lib/lib && \
EMCC_FLAGS=" -sSIDE_MODULE $JSPI_FLAGS" EMCC_SKIP="-lz" emmake make install_sw || true;
2 changes: 1 addition & 1 deletion packages/php-wasm/compile/libz/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,6 @@ RUN source /root/emsdk/emsdk_env.sh && \
emconfigure ./configure --prefix=/root/lib && \
# emmake make fails, but only after it builds the library files.
# Let's just ignore the errors and proceed with the built libraries.
export JSPI_FLAGS=$(if [ "$JSPI" = "1" ]; then echo "-sSUPPORT_LONGJMP=wasm -fwasm-exceptions"; else echo ""; fi) && \
export JSPI_FLAGS=$(if [ "$JSPI" = "1" ]; then echo "-sJSPI=1 -sJSPI_IMPORTS=close,fd_close,wasm_close -sJSPI_EXPORTS=close,fd_close,wasm_close -sSUPPORT_LONGJMP=wasm -fwasm-exceptions"; else echo ""; fi) && \
(EMCC_FLAGS=" -D__x86_64__ -sSIDE_MODULE $JSPI_FLAGS" emmake make || true) && \
EMCC_FLAGS=" -D__x86_64__ -sSIDE_MODULE $JSPI_FLAGS" emmake make install
8 changes: 7 additions & 1 deletion packages/php-wasm/compile/php/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2105,7 +2105,13 @@ RUN set -euxo pipefail; \
source /root/emsdk/emsdk_env.sh; \
if [ "$WITH_JSPI" = "yes" ]; then \
# Both imports and exports are required for inter-module communication with wrapped methods, e.g., wasm_recv.
export ASYNCIFY_FLAGS=" -s ASYNCIFY=2 -sSUPPORT_LONGJMP=wasm -fwasm-exceptions -sJSPI_IMPORTS=js_open_process,js_fd_read,js_waitpid,js_process_status,js_create_input_device,wasm_setsockopt,wasm_shutdown,wasm_close,wasm_recv,__syscall_fcntl64,js_flock,js_release_file_locks,js_waitpid,fd_close -sJSPI_EXPORTS=php_wasm_init,fd_close,wasm_sleep,wasm_read,emscripten_sleep,wasm_sapi_handle_request,wasm_sapi_request_shutdown,wasm_poll_socket,wrap_select,__wrap_select,select,php_pollfd_for,fflush,wasm_popen,wasm_read,wasm_php_exec,run_cli,wasm_recv -s EXTRA_EXPORTED_RUNTIME_METHODS=ccall,PROXYFS,wasmExports,_malloc,setErrNo "; \
export JSPI_ADD_IMPORTS=""; \
export JSPI_ADD_EXPORTS=""; \
if [ "$EMSCRIPTEN_ENVIRONMENT" = "node" ]; then \
export JSPI_ADD_IMPORTS=",fd_close"; \
export JSPI_ADD_EXPORTS=",fd_close"; \
fi; \
export ASYNCIFY_FLAGS=" -s ASYNCIFY=2 -sSUPPORT_LONGJMP=wasm -fwasm-exceptions -sJSPI_IMPORTS=js_open_process,js_fd_read,js_waitpid,js_process_status,js_create_input_device,wasm_setsockopt,wasm_shutdown,wasm_close,wasm_recv,__syscall_fcntl64,js_flock,js_release_file_locks,js_waitpid$JSPI_ADD_IMPORTS -sJSPI_EXPORTS=php_wasm_init,wasm_sleep,wasm_read,emscripten_sleep,wasm_sapi_handle_request,wasm_sapi_request_shutdown,wasm_poll_socket,wrap_select,__wrap_select,select,php_pollfd_for,fflush,wasm_popen,wasm_read,wasm_php_exec,run_cli,wasm_recv$JSPI_ADD_EXPORTS -s EXTRA_EXPORTED_RUNTIME_METHODS=ccall,PROXYFS,wasmExports,_malloc,setErrNo "; \
echo '#define PLAYGROUND_JSPI 1' > /root/php_wasm_asyncify.h; \
else \
export ASYNCIFY_FLAGS=" -s ASYNCIFY=1 -s ASYNCIFY_IGNORE_INDIRECT=1 -s EXPORTED_RUNTIME_METHODS=ccall,PROXYFS,wasmExports,setErrNo $(cat /root/.emcc-php-asyncify-flags) "; \
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSpawnHandler } from '@php-wasm/util';
import type { PHPProcessManager } from './php-process-manager';
import { logger } from '@php-wasm/logger';

/**
* An isomorphic proc_open() handler that implements typical shell in TypeScript
Expand Down Expand Up @@ -90,6 +91,7 @@ export function sandboxedSpawnHandlerFactory(
);
processApi.exit(await result.exitCode);
} catch (e) {
logger.error(e);
// An exception here means the PHP runtime has crashed.
processApi.exit(1);
throw e;
Expand Down
Binary file modified packages/php-wasm/web/public/php/jspi/8_3_25/php_8_3.wasm
Binary file not shown.
7,877 changes: 7,871 additions & 6 deletions packages/php-wasm/web/public/php/jspi/php_8_3.js

Large diffs are not rendered by default.

Binary file modified packages/playground/blueprints/blueprints.phar
Binary file not shown.
29 changes: 17 additions & 12 deletions packages/playground/blueprints/src/lib/v2/run-blueprint-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,12 @@ import {
type UniversalPHP,
} from '@php-wasm/universal';
import { phpVar } from '@php-wasm/util';
import { getV2Runner } from './get-v2-runner';
import {
type BlueprintV2Declaration,
type ParsedBlueprintV2Declaration,
parseBlueprintDeclaration,
} from './blueprint-v2-declaration';
import { getV2Runner } from './get-v2-runner';

export type PHPExceptionDetails = {
exception: string;
Expand Down Expand Up @@ -206,18 +206,23 @@ function playground_progress_reporter() {
return new PlaygroundProgressReporter();
}
playground_add_filter('blueprint.progress_reporter', 'playground_progress_reporter');

require( "/tmp/blueprints.phar" );
`
);
const streamedResponse = (await (php as any).cli([
'/internal/shared/bin/php',
'/tmp/run-blueprints.php',
'exec',
blueprintReference,
...cliArgs,
])) as StreamedPHPResponse;

streamedResponse.finished.finally(unbindMessageListener);

return streamedResponse;
try {
const streamedResponse = (await (php as any).cli([
'/internal/shared/bin/php',
'/tmp/run-blueprints.php',
'exec',
blueprintReference,
...cliArgs,
])) as StreamedPHPResponse;

await streamedResponse.exitCode;

return streamedResponse;
} finally {
unbindMessageListener();
}
}
107 changes: 72 additions & 35 deletions packages/playground/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,36 +1,36 @@
export * from '@wp-playground/blueprints';

export {
LatestSupportedPHPVersion,
setPhpIniEntries,
SupportedPHPVersions,
SupportedPHPVersionsList,
} from '@php-wasm/universal';
export type {
ErrnoError,
HTTPMethod,
PHPRunOptions,
PHPRequest,
PHPResponse,
UniversalPHP,
PHPOutput,
PHPResponseData,
ErrnoError,
PHPRequest,
PHPRequestHandler,
PHPRequestHandlerConfiguration,
PHPRequestHeaders,
SupportedPHPVersion,
PHPResponse,
PHPResponseData,
PHPRunOptions,
RmDirOptions,
RuntimeType,
} from '@php-wasm/universal';
export {
setPhpIniEntries,
SupportedPHPVersions,
SupportedPHPVersionsList,
LatestSupportedPHPVersion,
SupportedPHPVersion,
UniversalPHP,
} from '@php-wasm/universal';
export { phpVar, phpVars } from '@php-wasm/util';
export type { PlaygroundClient, MountDescriptor };
export type { MountDescriptor, PlaygroundClient };

import { collectPhpLogs, logger } from '@php-wasm/logger';
import { ProgressTracker } from '@php-wasm/progress';
import { consumeAPI } from '@php-wasm/web';
import type { Blueprint, OnStepCompleted } from '@wp-playground/blueprints';
import { compileBlueprint, runBlueprintSteps } from '@wp-playground/blueprints';
import { consumeAPI } from '@php-wasm/web';
import { ProgressTracker } from '@php-wasm/progress';
import type { MountDescriptor, PlaygroundClient } from '@wp-playground/remote';
import { collectPhpLogs, logger } from '@php-wasm/logger';
import { additionalRemoteOrigins } from './additional-remote-origins';
// eslint-disable-next-line @nx/enforce-module-boundaries
import { remoteDevServerHost, remoteDevServerPort } from '../../build-config';
Expand All @@ -41,6 +41,10 @@ export interface StartPlaygroundOptions {
progressTracker?: ProgressTracker;
disableProgressBar?: boolean;
blueprint?: Blueprint;
/**
* Prefer experimental Blueprints v2 PHP runner instead of TypeScript steps
*/
experimentalBlueprintsV2Runner?: boolean;
onBlueprintStepCompleted?: OnStepCompleted;
/**
* Called when the playground client is connected, but before the blueprint
Expand All @@ -56,14 +60,6 @@ export interface StartPlaygroundOptions {
* @private
*/
sapiName?: string;
/**
* Called before the blueprint steps are run,
* allows the caller to delay the Blueprint execution
* once the Playground is booted.
*
* @returns
*/
onBeforeBlueprint?: () => Promise<void>;
mounts?: Array<MountDescriptor>;
shouldInstallWordPress?: boolean;
/**
Expand Down Expand Up @@ -108,12 +104,12 @@ export async function startPlaygroundWeb({
onBlueprintStepCompleted,
onClientConnected = () => {},
sapiName,
onBeforeBlueprint,
mounts,
scope,
corsProxy,
shouldInstallWordPress,
sqliteDriverVersion,
experimentalBlueprintsV2Runner,
}: StartPlaygroundOptions): Promise<PlaygroundClient> {
assertLikelyCompatibleRemoteOrigin(remoteUrl);
allowStorageAccessByUserActivation(iframe);
Expand All @@ -128,17 +124,25 @@ export async function startPlaygroundWeb({
blueprint = {};
}

const compiled = await compileBlueprint(blueprint, {
progress: progressTracker.stage(0.5),
onStepCompleted: onBlueprintStepCompleted,
corsProxy,
});

await new Promise((resolve) => {
iframe.src = remoteUrl;
iframe.addEventListener('load', resolve, false);
});

const compiled = experimentalBlueprintsV2Runner
? await compileBlueprint(
{},
{
progress: progressTracker.stage(0.5),
corsProxy,
}
)
: await compileBlueprint(blueprint, {
progress: progressTracker.stage(0.5),
onStepCompleted: onBlueprintStepCompleted,
corsProxy,
});

// Connect the Comlink API client to the remote worker,
// boot the playground, and run the blueprint steps.
const playground = consumeAPI<PlaygroundClient>(
Expand All @@ -149,6 +153,29 @@ export async function startPlaygroundWeb({
progressTracker.pipe(playground);
const downloadPHPandWP = progressTracker.stage();
await playground.onDownloadProgress(downloadPHPandWP.loadingListener);

// Subscribe early to blueprint messages when v2 runner is enabled
if (experimentalBlueprintsV2Runner && blueprint) {
await playground.onBlueprintMessage(async (message: any) => {
if (message?.type === 'blueprint.progress') {
await playground.setProgress({
caption: (message.caption || 'Working').trim(),
progress: message.progress,
isIndefinite: false,
visible: true,
});
}
if (message?.type === 'blueprint.error') {
await playground.setProgress({
caption: 'Error',
isIndefinite: false,
visible: true,
progress: 100,
});
}
});
}

await playground.boot({
mounts,
sapiName,
Expand All @@ -160,18 +187,28 @@ export async function startPlaygroundWeb({
withNetworking: compiled.features.networking,
corsProxyUrl: corsProxy,
sqliteDriverVersion,
experimentalBlueprintsV2Runner,
blueprint: blueprint as any,
});
await playground.isReady();
downloadPHPandWP.finish();

collectPhpLogs(logger, playground);
onClientConnected(playground);

if (onBeforeBlueprint) {
await onBeforeBlueprint();
if (experimentalBlueprintsV2Runner) {
// @TODO: Source the landing page URL from the v2 Blueprint
// @TODO: Maybe reconcile this code path with v1 Blueprints? Right now it's
// handled in `compileBlueprint`. Perhaps we could somehow move it to a
// PHP plugin and also support it in Node.js? Or at least handle initial
// redirection for both v1 and v2 Blueprints in the same place in web browsers?
await playground.goTo('/');
} else if (blueprint) {
// Blueprints v1 runner.
// @TODO: Should we run this in remote instead?
await runBlueprintSteps(compiled, playground);
}

await runBlueprintSteps(compiled, playground);
/**
* Pre-fetch WordPress update checks to speed up the initial wp-admin load.
*
Expand Down
3 changes: 3 additions & 0 deletions packages/playground/remote/src/lib/boot-playground-remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,9 @@ export async function bootPlaygroundRemote() {
async removeEventListener(event, listener) {
return await phpWorkerApi.removeEventListener(event, listener);
},
async onBlueprintMessage(listener) {
return await (phpWorkerApi as any).onBlueprintMessage(listener);
},
async setProgress(options: ProgressBarOptions) {
if (!bar) {
throw new Error('Progress bar not available');
Expand Down
7 changes: 7 additions & 0 deletions packages/playground/remote/src/lib/playground-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ export interface WebClientMixin extends ProgressReceiver {
/** @inheritDoc @php-wasm/universal!UniversalPHP.onMessage */
onMessage(listener: MessageListener): Promise<() => Promise<void>>;

/**
* Subscribes to Blueprint v2 progress/error/completion messages.
*/
onBlueprintMessage(
listener: (message: any) => void | Promise<void>
): Promise<() => Promise<void>>;

mountOpfs(
options: MountDescriptor,
onProgress?: SyncProgressCallback
Expand Down
Loading
Loading