From 14f3eb19f6da43fe8f6f9f55d64a84df048f0671 Mon Sep 17 00:00:00 2001 From: Bero Date: Thu, 4 Jul 2024 11:00:26 +0200 Subject: [PATCH 1/4] Send startup options as message --- .../worker-thread/spawn-php-worker-thread.ts | 4 +++ .../playground/remote/src/lib/worker-utils.ts | 27 +++++++++++-------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/packages/php-wasm/web/src/lib/worker-thread/spawn-php-worker-thread.ts b/packages/php-wasm/web/src/lib/worker-thread/spawn-php-worker-thread.ts index c9e315087d..5713638014 100644 --- a/packages/php-wasm/web/src/lib/worker-thread/spawn-php-worker-thread.ts +++ b/packages/php-wasm/web/src/lib/worker-thread/spawn-php-worker-thread.ts @@ -21,6 +21,10 @@ export async function spawnPHPWorkerThread( (error as any).filename = e.filename; reject(error); }; + worker.postMessage({ + type: 'startup-options', + startupOptions, + }); // There is no way to know when the worker script has started // executing, so we use a message to signal that. function onStartup(event: { data: string }) { diff --git a/packages/playground/remote/src/lib/worker-utils.ts b/packages/playground/remote/src/lib/worker-utils.ts index c3f90e0dca..a5914ebfc1 100644 --- a/packages/playground/remote/src/lib/worker-utils.ts +++ b/packages/playground/remote/src/lib/worker-utils.ts @@ -33,18 +33,23 @@ export type ParsedStartupOptions = { }; export const receivedParams: ReceivedStartupOptions = {}; -const url = self?.location?.href; -if (typeof url !== 'undefined') { - const params = new URL(self.location.href).searchParams; - receivedParams.wpVersion = params.get('wpVersion') || undefined; - receivedParams.phpVersion = params.get('phpVersion') || undefined; - receivedParams.storage = params.get('storage') || undefined; - // Default to CLI to support the WP-CLI Blueprint step - receivedParams.sapiName = params.get('sapiName') || 'cli'; - receivedParams.phpExtensions = params.getAll('php-extension'); - receivedParams.siteSlug = params.get('site-slug') || undefined; -} +self.addEventListener('message', (event) => { + if (event.data?.type === 'startup-options') { + const { startupOptions } = event.data; + receivedParams.wpVersion = startupOptions.wpVersion; + receivedParams.phpVersion = startupOptions.phpVersion; + receivedParams.sapiName = startupOptions.sapiName; + receivedParams.storage = startupOptions.storage; + receivedParams.phpExtensions = startupOptions.phpExtensions; + receivedParams.siteSlug = startupOptions.siteSlug; + } +}); +// TODO find a better way to pass the startup options +while (!receivedParams.wpVersion) { + // wait for the next event loop + await new Promise((resolve) => setTimeout(resolve, 0)); +} export const requestedWPVersion = receivedParams.wpVersion || ''; export const startupOptions = { wpVersion: SupportedWordPressVersionsList.includes(requestedWPVersion) From 24f2eeb71d20268506bb59bdea389b0285bcc9c1 Mon Sep 17 00:00:00 2001 From: Bero Date: Thu, 4 Jul 2024 12:39:24 +0200 Subject: [PATCH 2/4] Load service worker after startup options are sent --- .../remote/src/lib/worker-thread.ts | 360 ++++++++++-------- .../playground/remote/src/lib/worker-utils.ts | 43 +-- 2 files changed, 205 insertions(+), 198 deletions(-) diff --git a/packages/playground/remote/src/lib/worker-thread.ts b/packages/playground/remote/src/lib/worker-thread.ts index beab3e7c77..a32562bcd8 100644 --- a/packages/playground/remote/src/lib/worker-thread.ts +++ b/packages/playground/remote/src/lib/worker-thread.ts @@ -8,6 +8,7 @@ import { LatestSupportedWordPressVersion, SupportedWordPressVersions, sqliteDatabaseIntegrationModuleDetails, + SupportedWordPressVersionsList, } from '@wp-playground/wordpress-builds'; import { @@ -17,12 +18,12 @@ import { } from './opfs/bind-opfs'; import { randomString } from '@php-wasm/util'; import { - requestedWPVersion, - startupOptions, monitoredFetch, downloadMonitor, spawnHandlerFactory, createPhpRuntime, + ReceivedStartupOptions, + ParsedStartupOptions, } from './worker-utils'; import { FilesystemOperation, @@ -35,7 +36,12 @@ import transportFetch from './playground-mu-plugin/playground-includes/wp_http_f import transportDummy from './playground-mu-plugin/playground-includes/wp_http_dummy.php?raw'; /** @ts-ignore */ import playgroundWebMuPlugin from './playground-mu-plugin/0-playground.php?raw'; -import { PHP, PHPWorker } from '@php-wasm/universal'; +import { + PHP, + PHPWorker, + SupportedPHPVersion, + SupportedPHPVersionsList, +} from '@php-wasm/universal'; import { bootWordPress, getLoadedWordPressVersion, @@ -54,52 +60,6 @@ let virtualOpfsDir: FileSystemDirectoryHandle | undefined; let lastOpfsHandle: FileSystemDirectoryHandle | undefined; let lastOpfsMountpoint: string | undefined; let wordPressAvailableInOPFS = false; -if ( - startupOptions.storage === 'browser' && - // @ts-ignore - typeof navigator?.storage?.getDirectory !== 'undefined' -) { - virtualOpfsRoot = await navigator.storage.getDirectory(); - virtualOpfsDir = await virtualOpfsRoot.getDirectoryHandle( - startupOptions.siteSlug === 'wordpress' - ? startupOptions.siteSlug - : 'site-' + startupOptions.siteSlug, - { - create: true, - } - ); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - lastOpfsHandle = virtualOpfsDir; - lastOpfsMountpoint = '/wordpress'; - wordPressAvailableInOPFS = await playgroundAvailableInOpfs(virtualOpfsDir!); -} - -// The SQLite integration must always be downloaded, even when using OPFS or Native FS, -// because it can't be assumed to exist in WordPress document root. Instead, it's installed -// in the /internal directory to avoid polluting the mounted directory structure. -downloadMonitor.expectAssets({ - [sqliteDatabaseIntegrationModuleDetails.url]: - sqliteDatabaseIntegrationModuleDetails.size, -}); -const sqliteIntegrationRequest = downloadMonitor.monitorFetch( - fetch(sqliteDatabaseIntegrationModuleDetails.url) -); - -// Start downloading WordPress if needed -let wordPressRequest = null; -if (!wordPressAvailableInOPFS) { - if (requestedWPVersion.startsWith('http')) { - // We don't know the size upfront, but we can still monitor the download. - // monitorFetch will read the content-length response header when available. - wordPressRequest = monitoredFetch(requestedWPVersion); - } else { - const wpDetails = getWordPressModuleDetails(startupOptions.wpVersion); - downloadMonitor.expectAssets({ - [wpDetails.url]: wpDetails.size, - }); - wordPressRequest = monitoredFetch(wpDetails.url); - } -} /** @inheritDoc PHPClient */ export class PlaygroundWorkerEndpoint extends PHPWorker { @@ -254,121 +214,205 @@ async function backfillStaticFilesRemovedFromMinifiedBuild(php: PHP) { } } -const apiEndpoint = new PlaygroundWorkerEndpoint( - downloadMonitor, - scope, - startupOptions.wpVersion -); -const [setApiReady, setAPIError] = exposeAPI(apiEndpoint); - -try { - const constants: Record = wordPressAvailableInOPFS - ? {} - : { - WP_DEBUG: true, - WP_DEBUG_LOG: true, - WP_DEBUG_DISPLAY: false, - AUTH_KEY: randomString(40), - SECURE_AUTH_KEY: randomString(40), - LOGGED_IN_KEY: randomString(40), - NONCE_KEY: randomString(40), - AUTH_SALT: randomString(40), - SECURE_AUTH_SALT: randomString(40), - LOGGED_IN_SALT: randomString(40), - NONCE_SALT: randomString(40), - }; - const wordPressZip = wordPressAvailableInOPFS - ? undefined - : new File([await (await wordPressRequest!).blob()], 'wp.zip'); - - const sqliteIntegrationPluginZip = new File( - [await (await sqliteIntegrationRequest).blob()], - 'sqlite.zip' - ); - - const requestHandler = await bootWordPress({ - siteUrl: setURLScope(wordPressSiteUrl, scope).toString(), - createPhpRuntime, - wordPressZip, - sqliteIntegrationPluginZip, - spawnHandler: spawnHandlerFactory, - sapiName: startupOptions.sapiName, - constants, - hooks: { - async beforeDatabaseSetup(php) { - if (virtualOpfsDir) { - await bindOpfs({ - php, - mountpoint: '/wordpress', - opfs: virtualOpfsDir!, - initialSyncDirection: wordPressAvailableInOPFS - ? 'opfs-to-memfs' - : 'memfs-to-opfs', - }); +self.addEventListener('message', async (event) => { + if (event.data?.type === 'startup-options') { + const receivedParams: ReceivedStartupOptions = {}; + receivedParams.wpVersion = event.data.startupOptions.wpVersion; + receivedParams.phpVersion = event.data.startupOptions.phpVersion; + receivedParams.sapiName = event.data.startupOptions.sapiName; + receivedParams.storage = event.data.startupOptions.storage; + receivedParams.phpExtensions = event.data.startupOptions.phpExtensions; + receivedParams.siteSlug = event.data.startupOptions.siteSlug; + + const startupOptions = { + wpVersion: SupportedWordPressVersionsList.includes( + receivedParams.wpVersion || '' + ) + ? receivedParams.wpVersion + : LatestSupportedWordPressVersion, + phpVersion: SupportedPHPVersionsList.includes( + receivedParams.phpVersion || '' + ) + ? (receivedParams.phpVersion as SupportedPHPVersion) + : '8.0', + sapiName: receivedParams.sapiName || 'cli', + storage: receivedParams.storage || 'local', + phpExtensions: receivedParams.phpExtensions || [], + siteSlug: receivedParams.siteSlug, + } as ParsedStartupOptions; + + if ( + startupOptions.storage === 'browser' && + // @ts-ignore + typeof navigator?.storage?.getDirectory !== 'undefined' + ) { + virtualOpfsRoot = await navigator.storage.getDirectory(); + virtualOpfsDir = await virtualOpfsRoot.getDirectoryHandle( + startupOptions.siteSlug === 'wordpress' + ? startupOptions.siteSlug + : 'site-' + startupOptions.siteSlug, + { + create: true, } - }, - }, - createFiles: { - '/internal/shared/mu-plugins': { - '1-playground-web.php': playgroundWebMuPlugin, - 'playground-includes': { - 'wp_http_dummy.php': transportDummy, - 'wp_http_fetch.php': transportFetch, - }, - }, - }, - }); - apiEndpoint.__internal_setRequestHandler(requestHandler); - - const primaryPhp = await requestHandler.getPrimaryPhp(); - await apiEndpoint.setPrimaryPHP(primaryPhp); - - // NOTE: We need to derive the loaded WP version or we might assume WP loaded - // from browser storage is the default version when it is actually something else. - // Incorrectly assuming WP version can break things like remote asset retrieval - // for minified WP builds. - apiEndpoint.loadedWordPressVersion = await getLoadedWordPressVersion( - requestHandler - ); - if ( - apiEndpoint.requestedWordPressVersion !== - apiEndpoint.loadedWordPressVersion - ) { - logger.warn( - `Loaded WordPress version (${apiEndpoint.loadedWordPressVersion}) differs ` + - `from requested version (${apiEndpoint.requestedWordPressVersion}).` + ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + lastOpfsHandle = virtualOpfsDir; + lastOpfsMountpoint = '/wordpress'; + wordPressAvailableInOPFS = await playgroundAvailableInOpfs( + virtualOpfsDir! + ); + } + + // The SQLite integration must always be downloaded, even when using OPFS or Native FS, + // because it can't be assumed to exist in WordPress document root. Instead, it's installed + // in the /internal directory to avoid polluting the mounted directory structure. + downloadMonitor.expectAssets({ + [sqliteDatabaseIntegrationModuleDetails.url]: + sqliteDatabaseIntegrationModuleDetails.size, + }); + const sqliteIntegrationRequest = downloadMonitor.monitorFetch( + fetch(sqliteDatabaseIntegrationModuleDetails.url) ); - } - const wpStaticAssetsDir = wpVersionToStaticAssetsDirectory( - apiEndpoint.loadedWordPressVersion - ); - const remoteAssetListPath = joinPaths( - requestHandler.documentRoot, - 'wordpress-remote-asset-paths' - ); - if ( - wpStaticAssetsDir !== undefined && - !primaryPhp.fileExists(remoteAssetListPath) - ) { - // The loaded WP release has a remote static assets dir - // but no remote asset listing, so we need to backfill the listing. - const listUrl = new URL( - joinPaths(wpStaticAssetsDir, 'wordpress-remote-asset-paths'), - wordPressSiteUrl + // Start downloading WordPress if needed + let wordPressRequest = null; + if (!wordPressAvailableInOPFS) { + if (receivedParams.wpVersion.startsWith('http')) { + // We don't know the size upfront, but we can still monitor the download. + // monitorFetch will read the content-length response header when available. + wordPressRequest = monitoredFetch(receivedParams.wpVersion); + } else { + const wpDetails = getWordPressModuleDetails( + startupOptions.wpVersion + ); + downloadMonitor.expectAssets({ + [wpDetails.url]: wpDetails.size, + }); + wordPressRequest = monitoredFetch(wpDetails.url); + } + } + + const apiEndpoint = new PlaygroundWorkerEndpoint( + downloadMonitor, + scope, + startupOptions.wpVersion ); + const [setApiReady, setAPIError] = exposeAPI(apiEndpoint); + try { - const remoteAssetPaths = await fetch(listUrl).then((res) => - res.text() + const constants: Record = wordPressAvailableInOPFS + ? {} + : { + WP_DEBUG: true, + WP_DEBUG_LOG: true, + WP_DEBUG_DISPLAY: false, + AUTH_KEY: randomString(40), + SECURE_AUTH_KEY: randomString(40), + LOGGED_IN_KEY: randomString(40), + NONCE_KEY: randomString(40), + AUTH_SALT: randomString(40), + SECURE_AUTH_SALT: randomString(40), + LOGGED_IN_SALT: randomString(40), + NONCE_SALT: randomString(40), + }; + const wordPressZip = wordPressAvailableInOPFS + ? undefined + : new File([await (await wordPressRequest!).blob()], 'wp.zip'); + + const sqliteIntegrationPluginZip = new File( + [await (await sqliteIntegrationRequest).blob()], + 'sqlite.zip' + ); + + const requestHandler = await bootWordPress({ + siteUrl: setURLScope(wordPressSiteUrl, scope).toString(), + createPhpRuntime: () => createPhpRuntime(startupOptions), + wordPressZip, + sqliteIntegrationPluginZip, + spawnHandler: spawnHandlerFactory, + sapiName: startupOptions.sapiName, + constants, + hooks: { + async beforeDatabaseSetup(php) { + if (virtualOpfsDir) { + await bindOpfs({ + php, + mountpoint: '/wordpress', + opfs: virtualOpfsDir!, + initialSyncDirection: wordPressAvailableInOPFS + ? 'opfs-to-memfs' + : 'memfs-to-opfs', + }); + } + }, + }, + createFiles: { + '/internal/shared/mu-plugins': { + '1-playground-web.php': playgroundWebMuPlugin, + 'playground-includes': { + 'wp_http_dummy.php': transportDummy, + 'wp_http_fetch.php': transportFetch, + }, + }, + }, + }); + apiEndpoint.__internal_setRequestHandler(requestHandler); + + const primaryPhp = await requestHandler.getPrimaryPhp(); + await apiEndpoint.setPrimaryPHP(primaryPhp); + + // NOTE: We need to derive the loaded WP version or we might assume WP loaded + // from browser storage is the default version when it is actually something else. + // Incorrectly assuming WP version can break things like remote asset retrieval + // for minified WP builds. + apiEndpoint.loadedWordPressVersion = + await getLoadedWordPressVersion(requestHandler); + if ( + apiEndpoint.requestedWordPressVersion !== + apiEndpoint.loadedWordPressVersion + ) { + logger.warn( + `Loaded WordPress version (${apiEndpoint.loadedWordPressVersion}) differs ` + + `from requested version (${apiEndpoint.requestedWordPressVersion}).` + ); + } + + const wpStaticAssetsDir = wpVersionToStaticAssetsDirectory( + apiEndpoint.loadedWordPressVersion + ); + const remoteAssetListPath = joinPaths( + requestHandler.documentRoot, + 'wordpress-remote-asset-paths' ); - primaryPhp.writeFile(remoteAssetListPath, remoteAssetPaths); + if ( + wpStaticAssetsDir !== undefined && + !primaryPhp.fileExists(remoteAssetListPath) + ) { + // The loaded WP release has a remote static assets dir + // but no remote asset listing, so we need to backfill the listing. + const listUrl = new URL( + joinPaths( + wpStaticAssetsDir, + 'wordpress-remote-asset-paths' + ), + wordPressSiteUrl + ); + try { + const remoteAssetPaths = await fetch(listUrl).then((res) => + res.text() + ); + primaryPhp.writeFile(remoteAssetListPath, remoteAssetPaths); + } catch (e) { + logger.warn( + `Failed to fetch remote asset paths from ${listUrl}` + ); + } + } + + setApiReady(); } catch (e) { - logger.warn(`Failed to fetch remote asset paths from ${listUrl}`); + setAPIError(e as Error); + throw e; } } - - setApiReady(); -} catch (e) { - setAPIError(e as Error); - throw e; -} +}); diff --git a/packages/playground/remote/src/lib/worker-utils.ts b/packages/playground/remote/src/lib/worker-utils.ts index a5914ebfc1..325cd4c76b 100644 --- a/packages/playground/remote/src/lib/worker-utils.ts +++ b/packages/playground/remote/src/lib/worker-utils.ts @@ -1,13 +1,8 @@ import { loadWebRuntime } from '@php-wasm/web'; -import { - LatestSupportedWordPressVersion, - SupportedWordPressVersionsList, -} from '@wp-playground/wordpress-builds'; import { PHPResponse, PHPProcessManager, SupportedPHPVersion, - SupportedPHPVersionsList, } from '@php-wasm/universal'; import { EmscriptenDownloadMonitor } from '@php-wasm/progress'; import { createSpawnHandler, phpVar } from '@php-wasm/util'; @@ -32,47 +27,15 @@ export type ParsedStartupOptions = { siteSlug: string; }; -export const receivedParams: ReceivedStartupOptions = {}; -self.addEventListener('message', (event) => { - if (event.data?.type === 'startup-options') { - const { startupOptions } = event.data; - receivedParams.wpVersion = startupOptions.wpVersion; - receivedParams.phpVersion = startupOptions.phpVersion; - receivedParams.sapiName = startupOptions.sapiName; - receivedParams.storage = startupOptions.storage; - receivedParams.phpExtensions = startupOptions.phpExtensions; - receivedParams.siteSlug = startupOptions.siteSlug; - } -}); - -// TODO find a better way to pass the startup options -while (!receivedParams.wpVersion) { - // wait for the next event loop - await new Promise((resolve) => setTimeout(resolve, 0)); -} -export const requestedWPVersion = receivedParams.wpVersion || ''; -export const startupOptions = { - wpVersion: SupportedWordPressVersionsList.includes(requestedWPVersion) - ? requestedWPVersion - : LatestSupportedWordPressVersion, - phpVersion: SupportedPHPVersionsList.includes( - receivedParams.phpVersion || '' - ) - ? (receivedParams.phpVersion as SupportedPHPVersion) - : '8.0', - sapiName: receivedParams.sapiName || 'cli', - storage: receivedParams.storage || 'local', - phpExtensions: receivedParams.phpExtensions || [], - siteSlug: receivedParams.siteSlug, -} as ParsedStartupOptions; - export const downloadMonitor = new EmscriptenDownloadMonitor(); export const monitoredFetch = (input: RequestInfo | URL, init?: RequestInit) => downloadMonitor.monitorFetch(fetch(input, init)); const memoizedFetch = createMemoizedFetch(monitoredFetch); -export const createPhpRuntime = async () => { +export const createPhpRuntime = async ( + startupOptions: ParsedStartupOptions +) => { let wasmUrl = ''; return await loadWebRuntime(startupOptions.phpVersion, { onPhpLoaderModuleLoaded: (phpLoaderModule) => { From 15717aa1def49b00f343a778854c4328ca4b864c Mon Sep 17 00:00:00 2001 From: Bero Date: Thu, 4 Jul 2024 13:14:00 +0200 Subject: [PATCH 3/4] Linter fix --- packages/playground/remote/src/lib/worker-thread.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/playground/remote/src/lib/worker-thread.ts b/packages/playground/remote/src/lib/worker-thread.ts index a32562bcd8..b75ba3a668 100644 --- a/packages/playground/remote/src/lib/worker-thread.ts +++ b/packages/playground/remote/src/lib/worker-thread.ts @@ -277,10 +277,10 @@ self.addEventListener('message', async (event) => { // Start downloading WordPress if needed let wordPressRequest = null; if (!wordPressAvailableInOPFS) { - if (receivedParams.wpVersion.startsWith('http')) { + if (startupOptions.wpVersion.startsWith('http')) { // We don't know the size upfront, but we can still monitor the download. // monitorFetch will read the content-length response header when available. - wordPressRequest = monitoredFetch(receivedParams.wpVersion); + wordPressRequest = monitoredFetch(startupOptions.wpVersion); } else { const wpDetails = getWordPressModuleDetails( startupOptions.wpVersion From 7c69b09c1775685017bd833442ed2fcf2ff65560 Mon Sep 17 00:00:00 2001 From: Bero Date: Fri, 5 Jul 2024 07:35:11 +0200 Subject: [PATCH 4/4] Await for params in worker-utils --- .../remote/src/lib/worker-thread.ts | 360 ++++++++---------- .../playground/remote/src/lib/worker-utils.ts | 44 ++- 2 files changed, 199 insertions(+), 205 deletions(-) diff --git a/packages/playground/remote/src/lib/worker-thread.ts b/packages/playground/remote/src/lib/worker-thread.ts index b75ba3a668..beab3e7c77 100644 --- a/packages/playground/remote/src/lib/worker-thread.ts +++ b/packages/playground/remote/src/lib/worker-thread.ts @@ -8,7 +8,6 @@ import { LatestSupportedWordPressVersion, SupportedWordPressVersions, sqliteDatabaseIntegrationModuleDetails, - SupportedWordPressVersionsList, } from '@wp-playground/wordpress-builds'; import { @@ -18,12 +17,12 @@ import { } from './opfs/bind-opfs'; import { randomString } from '@php-wasm/util'; import { + requestedWPVersion, + startupOptions, monitoredFetch, downloadMonitor, spawnHandlerFactory, createPhpRuntime, - ReceivedStartupOptions, - ParsedStartupOptions, } from './worker-utils'; import { FilesystemOperation, @@ -36,12 +35,7 @@ import transportFetch from './playground-mu-plugin/playground-includes/wp_http_f import transportDummy from './playground-mu-plugin/playground-includes/wp_http_dummy.php?raw'; /** @ts-ignore */ import playgroundWebMuPlugin from './playground-mu-plugin/0-playground.php?raw'; -import { - PHP, - PHPWorker, - SupportedPHPVersion, - SupportedPHPVersionsList, -} from '@php-wasm/universal'; +import { PHP, PHPWorker } from '@php-wasm/universal'; import { bootWordPress, getLoadedWordPressVersion, @@ -60,6 +54,52 @@ let virtualOpfsDir: FileSystemDirectoryHandle | undefined; let lastOpfsHandle: FileSystemDirectoryHandle | undefined; let lastOpfsMountpoint: string | undefined; let wordPressAvailableInOPFS = false; +if ( + startupOptions.storage === 'browser' && + // @ts-ignore + typeof navigator?.storage?.getDirectory !== 'undefined' +) { + virtualOpfsRoot = await navigator.storage.getDirectory(); + virtualOpfsDir = await virtualOpfsRoot.getDirectoryHandle( + startupOptions.siteSlug === 'wordpress' + ? startupOptions.siteSlug + : 'site-' + startupOptions.siteSlug, + { + create: true, + } + ); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + lastOpfsHandle = virtualOpfsDir; + lastOpfsMountpoint = '/wordpress'; + wordPressAvailableInOPFS = await playgroundAvailableInOpfs(virtualOpfsDir!); +} + +// The SQLite integration must always be downloaded, even when using OPFS or Native FS, +// because it can't be assumed to exist in WordPress document root. Instead, it's installed +// in the /internal directory to avoid polluting the mounted directory structure. +downloadMonitor.expectAssets({ + [sqliteDatabaseIntegrationModuleDetails.url]: + sqliteDatabaseIntegrationModuleDetails.size, +}); +const sqliteIntegrationRequest = downloadMonitor.monitorFetch( + fetch(sqliteDatabaseIntegrationModuleDetails.url) +); + +// Start downloading WordPress if needed +let wordPressRequest = null; +if (!wordPressAvailableInOPFS) { + if (requestedWPVersion.startsWith('http')) { + // We don't know the size upfront, but we can still monitor the download. + // monitorFetch will read the content-length response header when available. + wordPressRequest = monitoredFetch(requestedWPVersion); + } else { + const wpDetails = getWordPressModuleDetails(startupOptions.wpVersion); + downloadMonitor.expectAssets({ + [wpDetails.url]: wpDetails.size, + }); + wordPressRequest = monitoredFetch(wpDetails.url); + } +} /** @inheritDoc PHPClient */ export class PlaygroundWorkerEndpoint extends PHPWorker { @@ -214,205 +254,121 @@ async function backfillStaticFilesRemovedFromMinifiedBuild(php: PHP) { } } -self.addEventListener('message', async (event) => { - if (event.data?.type === 'startup-options') { - const receivedParams: ReceivedStartupOptions = {}; - receivedParams.wpVersion = event.data.startupOptions.wpVersion; - receivedParams.phpVersion = event.data.startupOptions.phpVersion; - receivedParams.sapiName = event.data.startupOptions.sapiName; - receivedParams.storage = event.data.startupOptions.storage; - receivedParams.phpExtensions = event.data.startupOptions.phpExtensions; - receivedParams.siteSlug = event.data.startupOptions.siteSlug; - - const startupOptions = { - wpVersion: SupportedWordPressVersionsList.includes( - receivedParams.wpVersion || '' - ) - ? receivedParams.wpVersion - : LatestSupportedWordPressVersion, - phpVersion: SupportedPHPVersionsList.includes( - receivedParams.phpVersion || '' - ) - ? (receivedParams.phpVersion as SupportedPHPVersion) - : '8.0', - sapiName: receivedParams.sapiName || 'cli', - storage: receivedParams.storage || 'local', - phpExtensions: receivedParams.phpExtensions || [], - siteSlug: receivedParams.siteSlug, - } as ParsedStartupOptions; - - if ( - startupOptions.storage === 'browser' && - // @ts-ignore - typeof navigator?.storage?.getDirectory !== 'undefined' - ) { - virtualOpfsRoot = await navigator.storage.getDirectory(); - virtualOpfsDir = await virtualOpfsRoot.getDirectoryHandle( - startupOptions.siteSlug === 'wordpress' - ? startupOptions.siteSlug - : 'site-' + startupOptions.siteSlug, - { - create: true, +const apiEndpoint = new PlaygroundWorkerEndpoint( + downloadMonitor, + scope, + startupOptions.wpVersion +); +const [setApiReady, setAPIError] = exposeAPI(apiEndpoint); + +try { + const constants: Record = wordPressAvailableInOPFS + ? {} + : { + WP_DEBUG: true, + WP_DEBUG_LOG: true, + WP_DEBUG_DISPLAY: false, + AUTH_KEY: randomString(40), + SECURE_AUTH_KEY: randomString(40), + LOGGED_IN_KEY: randomString(40), + NONCE_KEY: randomString(40), + AUTH_SALT: randomString(40), + SECURE_AUTH_SALT: randomString(40), + LOGGED_IN_SALT: randomString(40), + NONCE_SALT: randomString(40), + }; + const wordPressZip = wordPressAvailableInOPFS + ? undefined + : new File([await (await wordPressRequest!).blob()], 'wp.zip'); + + const sqliteIntegrationPluginZip = new File( + [await (await sqliteIntegrationRequest).blob()], + 'sqlite.zip' + ); + + const requestHandler = await bootWordPress({ + siteUrl: setURLScope(wordPressSiteUrl, scope).toString(), + createPhpRuntime, + wordPressZip, + sqliteIntegrationPluginZip, + spawnHandler: spawnHandlerFactory, + sapiName: startupOptions.sapiName, + constants, + hooks: { + async beforeDatabaseSetup(php) { + if (virtualOpfsDir) { + await bindOpfs({ + php, + mountpoint: '/wordpress', + opfs: virtualOpfsDir!, + initialSyncDirection: wordPressAvailableInOPFS + ? 'opfs-to-memfs' + : 'memfs-to-opfs', + }); } - ); - // eslint-disable-next-line @typescript-eslint/no-unused-vars - lastOpfsHandle = virtualOpfsDir; - lastOpfsMountpoint = '/wordpress'; - wordPressAvailableInOPFS = await playgroundAvailableInOpfs( - virtualOpfsDir! - ); - } - - // The SQLite integration must always be downloaded, even when using OPFS or Native FS, - // because it can't be assumed to exist in WordPress document root. Instead, it's installed - // in the /internal directory to avoid polluting the mounted directory structure. - downloadMonitor.expectAssets({ - [sqliteDatabaseIntegrationModuleDetails.url]: - sqliteDatabaseIntegrationModuleDetails.size, - }); - const sqliteIntegrationRequest = downloadMonitor.monitorFetch( - fetch(sqliteDatabaseIntegrationModuleDetails.url) + }, + }, + createFiles: { + '/internal/shared/mu-plugins': { + '1-playground-web.php': playgroundWebMuPlugin, + 'playground-includes': { + 'wp_http_dummy.php': transportDummy, + 'wp_http_fetch.php': transportFetch, + }, + }, + }, + }); + apiEndpoint.__internal_setRequestHandler(requestHandler); + + const primaryPhp = await requestHandler.getPrimaryPhp(); + await apiEndpoint.setPrimaryPHP(primaryPhp); + + // NOTE: We need to derive the loaded WP version or we might assume WP loaded + // from browser storage is the default version when it is actually something else. + // Incorrectly assuming WP version can break things like remote asset retrieval + // for minified WP builds. + apiEndpoint.loadedWordPressVersion = await getLoadedWordPressVersion( + requestHandler + ); + if ( + apiEndpoint.requestedWordPressVersion !== + apiEndpoint.loadedWordPressVersion + ) { + logger.warn( + `Loaded WordPress version (${apiEndpoint.loadedWordPressVersion}) differs ` + + `from requested version (${apiEndpoint.requestedWordPressVersion}).` ); + } - // Start downloading WordPress if needed - let wordPressRequest = null; - if (!wordPressAvailableInOPFS) { - if (startupOptions.wpVersion.startsWith('http')) { - // We don't know the size upfront, but we can still monitor the download. - // monitorFetch will read the content-length response header when available. - wordPressRequest = monitoredFetch(startupOptions.wpVersion); - } else { - const wpDetails = getWordPressModuleDetails( - startupOptions.wpVersion - ); - downloadMonitor.expectAssets({ - [wpDetails.url]: wpDetails.size, - }); - wordPressRequest = monitoredFetch(wpDetails.url); - } - } - - const apiEndpoint = new PlaygroundWorkerEndpoint( - downloadMonitor, - scope, - startupOptions.wpVersion + const wpStaticAssetsDir = wpVersionToStaticAssetsDirectory( + apiEndpoint.loadedWordPressVersion + ); + const remoteAssetListPath = joinPaths( + requestHandler.documentRoot, + 'wordpress-remote-asset-paths' + ); + if ( + wpStaticAssetsDir !== undefined && + !primaryPhp.fileExists(remoteAssetListPath) + ) { + // The loaded WP release has a remote static assets dir + // but no remote asset listing, so we need to backfill the listing. + const listUrl = new URL( + joinPaths(wpStaticAssetsDir, 'wordpress-remote-asset-paths'), + wordPressSiteUrl ); - const [setApiReady, setAPIError] = exposeAPI(apiEndpoint); - try { - const constants: Record = wordPressAvailableInOPFS - ? {} - : { - WP_DEBUG: true, - WP_DEBUG_LOG: true, - WP_DEBUG_DISPLAY: false, - AUTH_KEY: randomString(40), - SECURE_AUTH_KEY: randomString(40), - LOGGED_IN_KEY: randomString(40), - NONCE_KEY: randomString(40), - AUTH_SALT: randomString(40), - SECURE_AUTH_SALT: randomString(40), - LOGGED_IN_SALT: randomString(40), - NONCE_SALT: randomString(40), - }; - const wordPressZip = wordPressAvailableInOPFS - ? undefined - : new File([await (await wordPressRequest!).blob()], 'wp.zip'); - - const sqliteIntegrationPluginZip = new File( - [await (await sqliteIntegrationRequest).blob()], - 'sqlite.zip' - ); - - const requestHandler = await bootWordPress({ - siteUrl: setURLScope(wordPressSiteUrl, scope).toString(), - createPhpRuntime: () => createPhpRuntime(startupOptions), - wordPressZip, - sqliteIntegrationPluginZip, - spawnHandler: spawnHandlerFactory, - sapiName: startupOptions.sapiName, - constants, - hooks: { - async beforeDatabaseSetup(php) { - if (virtualOpfsDir) { - await bindOpfs({ - php, - mountpoint: '/wordpress', - opfs: virtualOpfsDir!, - initialSyncDirection: wordPressAvailableInOPFS - ? 'opfs-to-memfs' - : 'memfs-to-opfs', - }); - } - }, - }, - createFiles: { - '/internal/shared/mu-plugins': { - '1-playground-web.php': playgroundWebMuPlugin, - 'playground-includes': { - 'wp_http_dummy.php': transportDummy, - 'wp_http_fetch.php': transportFetch, - }, - }, - }, - }); - apiEndpoint.__internal_setRequestHandler(requestHandler); - - const primaryPhp = await requestHandler.getPrimaryPhp(); - await apiEndpoint.setPrimaryPHP(primaryPhp); - - // NOTE: We need to derive the loaded WP version or we might assume WP loaded - // from browser storage is the default version when it is actually something else. - // Incorrectly assuming WP version can break things like remote asset retrieval - // for minified WP builds. - apiEndpoint.loadedWordPressVersion = - await getLoadedWordPressVersion(requestHandler); - if ( - apiEndpoint.requestedWordPressVersion !== - apiEndpoint.loadedWordPressVersion - ) { - logger.warn( - `Loaded WordPress version (${apiEndpoint.loadedWordPressVersion}) differs ` + - `from requested version (${apiEndpoint.requestedWordPressVersion}).` - ); - } - - const wpStaticAssetsDir = wpVersionToStaticAssetsDirectory( - apiEndpoint.loadedWordPressVersion - ); - const remoteAssetListPath = joinPaths( - requestHandler.documentRoot, - 'wordpress-remote-asset-paths' + const remoteAssetPaths = await fetch(listUrl).then((res) => + res.text() ); - if ( - wpStaticAssetsDir !== undefined && - !primaryPhp.fileExists(remoteAssetListPath) - ) { - // The loaded WP release has a remote static assets dir - // but no remote asset listing, so we need to backfill the listing. - const listUrl = new URL( - joinPaths( - wpStaticAssetsDir, - 'wordpress-remote-asset-paths' - ), - wordPressSiteUrl - ); - try { - const remoteAssetPaths = await fetch(listUrl).then((res) => - res.text() - ); - primaryPhp.writeFile(remoteAssetListPath, remoteAssetPaths); - } catch (e) { - logger.warn( - `Failed to fetch remote asset paths from ${listUrl}` - ); - } - } - - setApiReady(); + primaryPhp.writeFile(remoteAssetListPath, remoteAssetPaths); } catch (e) { - setAPIError(e as Error); - throw e; + logger.warn(`Failed to fetch remote asset paths from ${listUrl}`); } } -}); + + setApiReady(); +} catch (e) { + setAPIError(e as Error); + throw e; +} diff --git a/packages/playground/remote/src/lib/worker-utils.ts b/packages/playground/remote/src/lib/worker-utils.ts index 325cd4c76b..e2a6b4010f 100644 --- a/packages/playground/remote/src/lib/worker-utils.ts +++ b/packages/playground/remote/src/lib/worker-utils.ts @@ -1,8 +1,13 @@ import { loadWebRuntime } from '@php-wasm/web'; +import { + LatestSupportedWordPressVersion, + SupportedWordPressVersionsList, +} from '@wp-playground/wordpress-builds'; import { PHPResponse, PHPProcessManager, SupportedPHPVersion, + SupportedPHPVersionsList, } from '@php-wasm/universal'; import { EmscriptenDownloadMonitor } from '@php-wasm/progress'; import { createSpawnHandler, phpVar } from '@php-wasm/util'; @@ -27,15 +32,48 @@ export type ParsedStartupOptions = { siteSlug: string; }; +const getReceivedParams = async () => { + return new Promise((resolve) => { + self.addEventListener('message', async (event) => { + if (event.data.type === 'startup-options') { + resolve({ + wpVersion: event.data.startupOptions.wpVersion, + phpVersion: event.data.startupOptions.phpVersion, + storage: event.data.startupOptions.storage, + sapiName: event.data.startupOptions.sapiName, + phpExtensions: event.data.startupOptions['php-extension'], + siteSlug: event.data.startupOptions['site-slug'], + }); + } + }); + }); +}; + +const receivedParams: ReceivedStartupOptions = await getReceivedParams(); + +export const requestedWPVersion = receivedParams.wpVersion || ''; +export const startupOptions = { + wpVersion: SupportedWordPressVersionsList.includes(requestedWPVersion) + ? requestedWPVersion + : LatestSupportedWordPressVersion, + phpVersion: SupportedPHPVersionsList.includes( + receivedParams.phpVersion || '' + ) + ? (receivedParams.phpVersion as SupportedPHPVersion) + : '8.0', + sapiName: receivedParams.sapiName || 'cli', + storage: receivedParams.storage || 'local', + phpExtensions: receivedParams.phpExtensions || [], + siteSlug: receivedParams.siteSlug, +} as ParsedStartupOptions; + export const downloadMonitor = new EmscriptenDownloadMonitor(); export const monitoredFetch = (input: RequestInfo | URL, init?: RequestInit) => downloadMonitor.monitorFetch(fetch(input, init)); const memoizedFetch = createMemoizedFetch(monitoredFetch); -export const createPhpRuntime = async ( - startupOptions: ParsedStartupOptions -) => { +export const createPhpRuntime = async () => { let wasmUrl = ''; return await loadWebRuntime(startupOptions.phpVersion, { onPhpLoaderModuleLoaded: (phpLoaderModule) => {