diff --git a/packages/playground/remote/service-worker.ts b/packages/playground/remote/service-worker.ts index 54547408a2..cf2cad94ba 100644 --- a/packages/playground/remote/service-worker.ts +++ b/packages/playground/remote/service-worker.ts @@ -49,13 +49,20 @@ initializeServiceWorker({ return emptyHtml(); } - const { staticAssetsDirectory } = await getScopedWpDetails(scope!); - const workerResponse = await convertFetchEventToPHPRequest(event); if ( workerResponse.status === 404 && workerResponse.headers.get('x-file-type') === 'static' ) { + const { staticAssetsDirectory } = await getScopedWpDetails( + scope! + ); + if (!staticAssetsDirectory) { + const plain404Response = workerResponse.clone(); + plain404Response.headers.delete('x-file-type'); + return plain404Response; + } + // If we get a 404 for a static file, try to fetch it from // the from the static assets directory at the remote server. const requestedUrl = new URL(event.request.url); @@ -230,7 +237,7 @@ function emptyHtml() { } type WPModuleDetails = { - staticAssetsDirectory: string; + staticAssetsDirectory?: string; }; const scopeToWpModule: Record = {}; diff --git a/packages/playground/remote/src/lib/worker-thread.ts b/packages/playground/remote/src/lib/worker-thread.ts index 007df12cf2..4ec50aae74 100644 --- a/packages/playground/remote/src/lib/worker-thread.ts +++ b/packages/playground/remote/src/lib/worker-thread.ts @@ -35,7 +35,12 @@ import transportDummy from './playground-mu-plugin/playground-includes/wp_http_d /** @ts-ignore */ import playgroundWebMuPlugin from './playground-mu-plugin/0-playground.php?raw'; import { PHPWorker } from '@php-wasm/universal'; -import { bootWordPress } from '@wp-playground/wordpress'; +import { + bootWordPress, + getLoadedWordPressVersion, + isSupportedWordPressVersion, +} from '@wp-playground/wordpress'; +import { logger } from '@php-wasm/logger'; const scope = Math.random().toFixed(16); @@ -102,18 +107,23 @@ export class PlaygroundWorkerEndpoint extends PHPWorker { scope: string; /** - * A string representing the version of WordPress being used. + * A string representing the requested version of WordPress. + */ + requestedWordPressVersion: string; + + /** + * A string representing the version of WordPress that was loaded. */ - wordPressVersion: string; + loadedWordPressVersion: string | undefined; constructor( monitor: EmscriptenDownloadMonitor, scope: string, - wordPressVersion: string + requestedWordPressVersion: string ) { super(undefined, monitor); this.scope = scope; - this.wordPressVersion = wordPressVersion; + this.requestedWordPressVersion = requestedWordPressVersion; } /** @@ -121,11 +131,13 @@ export class PlaygroundWorkerEndpoint extends PHPWorker { */ async getWordPressModuleDetails() { return { - majorVersion: this.wordPressVersion, - staticAssetsDirectory: `wp-${this.wordPressVersion.replace( - '_', - '.' - )}`, + majorVersion: + this.loadedWordPressVersion || this.requestedWordPressVersion, + staticAssetsDirectory: + this.loadedWordPressVersion && + isSupportedWordPressVersion(this.loadedWordPressVersion) + ? `wp-${this.loadedWordPressVersion}` + : undefined, }; } @@ -246,6 +258,23 @@ try { 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}).` + ); + } + setApiReady(); } catch (e) { setAPIError(e as Error); diff --git a/packages/playground/wordpress/src/index.ts b/packages/playground/wordpress/src/index.ts index 9948b50b4d..2fbdb2c0a9 100644 --- a/packages/playground/wordpress/src/index.ts +++ b/packages/playground/wordpress/src/index.ts @@ -2,6 +2,10 @@ import { PHP, UniversalPHP } from '@php-wasm/universal'; import { joinPaths, phpVar } from '@php-wasm/util'; import { unzipFile } from '@wp-playground/common'; export { bootWordPress } from './boot'; +export { + getLoadedWordPressVersion, + isSupportedWordPressVersion, +} from './version-detect'; export * from './rewrite-rules'; diff --git a/packages/playground/wordpress/src/test/version-detect.spec.ts b/packages/playground/wordpress/src/test/version-detect.spec.ts new file mode 100644 index 0000000000..6a7a64b1c5 --- /dev/null +++ b/packages/playground/wordpress/src/test/version-detect.spec.ts @@ -0,0 +1,97 @@ +import { + SupportedWordPressVersions, + getSqliteDatabaseModule, + getWordPressModule, +} from '@wp-playground/wordpress-builds'; +import { RecommendedPHPVersion } from '@wp-playground/common'; +import { loadNodeRuntime } from '@php-wasm/node'; +import { bootWordPress } from '../boot'; +import { + getLoadedWordPressVersion, + versionStringToLoadedWordPressVersion, +} from '../version-detect'; + +describe('Test WP version detection', async () => { + for (const expectedWordPressVersion of Object.keys( + SupportedWordPressVersions + )) { + it(`detects WP ${expectedWordPressVersion} at runtime`, async () => { + const handler = await bootWordPress({ + createPhpRuntime: async () => + await loadNodeRuntime(RecommendedPHPVersion), + siteUrl: 'http://playground-domain/', + wordPressZip: await getWordPressModule( + expectedWordPressVersion + ), + sqliteIntegrationPluginZip: await getSqliteDatabaseModule(), + }); + const loadedWordPressVersion = await getLoadedWordPressVersion( + handler + ); + expect(loadedWordPressVersion).to.equal(expectedWordPressVersion); + }); + } + + it('errors when unable to read version at runtime', async () => { + const handler = await bootWordPress({ + createPhpRuntime: async () => + await loadNodeRuntime(RecommendedPHPVersion), + siteUrl: 'http://playground-domain/', + wordPressZip: await getWordPressModule(), + sqliteIntegrationPluginZip: await getSqliteDatabaseModule(), + }); + const php = await handler.getPrimaryPhp(); + + php.unlink(`${handler.documentRoot}/wp-includes/version.php`); + const detectionResult = await getLoadedWordPressVersion(handler).then( + () => 'no-error', + () => 'error' + ); + expect(detectionResult).to.equal('error'); + }); + + it('errors on reading empty version at runtime', async () => { + const handler = await bootWordPress({ + createPhpRuntime: async () => + await loadNodeRuntime(RecommendedPHPVersion), + siteUrl: 'http://playground-domain/', + wordPressZip: await getWordPressModule(), + sqliteIntegrationPluginZip: await getSqliteDatabaseModule(), + }); + const php = await handler.getPrimaryPhp(); + + php.writeFile( + `${handler.documentRoot}/wp-includes/version.php`, + ' 'no-error', + () => 'error' + ); + expect(detectionResult).to.equal('error'); + }); + + const versionMap = { + '6.3': '6.3', + '6.4.2': '6.4', + '6.5': '6.5', + '6.5.4': '6.5', + '6.6-alpha-57783': 'nightly', + '6.6-beta-57783': 'nightly', + '6.6-RC-54321': 'nightly', + '6.6-RC2-12345': 'nightly', + '6.6-beta': 'beta', + '6.6-beta2': 'beta', + '6.6-RC': 'beta', + '6.6-RC2': 'beta', + 'custom-version': 'custom-version', + }; + + for (const [input, expected] of Object.entries(versionMap)) { + it(`maps '${input}' to '${expected}'`, () => { + const result = versionStringToLoadedWordPressVersion(input); + expect(result).to.equal(expected); + }); + } +}); diff --git a/packages/playground/wordpress/src/version-detect.ts b/packages/playground/wordpress/src/version-detect.ts new file mode 100644 index 0000000000..2c1762f688 --- /dev/null +++ b/packages/playground/wordpress/src/version-detect.ts @@ -0,0 +1,50 @@ +import type { PHPRequestHandler } from '@php-wasm/universal'; +import { SupportedWordPressVersions } from '@wp-playground/wordpress-builds'; + +export async function getLoadedWordPressVersion( + requestHandler: PHPRequestHandler +): Promise { + const php = await requestHandler.getPrimaryPhp(); + const result = await php.run({ + code: `