From 8bb36bed943f943187e941db2d9a299aed726dad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 6 Feb 2024 16:32:42 +0100 Subject: [PATCH 1/6] Snapshot Import Protocol v1 --- packages/php-wasm/universal/src/lib/index.ts | 2 +- packages/playground/blueprints/src/index.ts | 6 + .../lib/playground-mu-plugin/0-playground.php | 0 .../playground-includes/wp_http_dummy.php | 0 .../playground-includes/wp_http_fetch.php | 0 .../blueprints/src/lib/setup-mu-plugins.ts | 47 +++ .../src/lib/steps/import-wordpress-files.ts | 353 ++++++++++++++---- .../lib/steps/run-wp-installation-wizard.ts | 4 +- .../playground/blueprints/tsconfig.lib.json | 2 +- packages/playground/remote/src/lib/utils.ts | 8 - .../remote/src/lib/worker-thread.ts | 51 +-- packages/playground/remote/tsconfig.lib.json | 6 +- .../playground/wordpress/build/Dockerfile | 2 + .../playground/wordpress/tsconfig.lib.json | 2 +- 14 files changed, 362 insertions(+), 121 deletions(-) rename packages/playground/{remote => blueprints}/src/lib/playground-mu-plugin/0-playground.php (100%) rename packages/playground/{remote => blueprints}/src/lib/playground-mu-plugin/playground-includes/wp_http_dummy.php (100%) rename packages/playground/{remote => blueprints}/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php (100%) create mode 100644 packages/playground/blueprints/src/lib/setup-mu-plugins.ts delete mode 100644 packages/playground/remote/src/lib/utils.ts diff --git a/packages/php-wasm/universal/src/lib/index.ts b/packages/php-wasm/universal/src/lib/index.ts index c88a05501f3..074ddb5eb34 100644 --- a/packages/php-wasm/universal/src/lib/index.ts +++ b/packages/php-wasm/universal/src/lib/index.ts @@ -41,7 +41,7 @@ export type { SupportedPHPExtensionBundle, } from './supported-php-extensions'; export { BasePHP, __private__dont__use } from './base-php'; -export { loadPHPRuntime } from './load-php-runtime'; +export { currentJsRuntime, loadPHPRuntime } from './load-php-runtime'; export type { DataModule, EmscriptenOptions, diff --git a/packages/playground/blueprints/src/index.ts b/packages/playground/blueprints/src/index.ts index 48623ec67cf..2b203cc7363 100644 --- a/packages/playground/blueprints/src/index.ts +++ b/packages/playground/blueprints/src/index.ts @@ -3,6 +3,12 @@ import '@php-wasm/node-polyfills'; export * from './lib/steps'; export * from './lib/steps/handlers'; +export { + linkSnapshot, + setSnapshot, + backfillSqliteMuPlugin, +} from './lib/steps/import-wordpress-files'; +export { installPlaygroundMuPlugin } from './lib/setup-mu-plugins'; export { runBlueprintSteps, compileBlueprint } from './lib/compile'; export type { Blueprint } from './lib/blueprint'; export type { diff --git a/packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php b/packages/playground/blueprints/src/lib/playground-mu-plugin/0-playground.php similarity index 100% rename from packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php rename to packages/playground/blueprints/src/lib/playground-mu-plugin/0-playground.php diff --git a/packages/playground/remote/src/lib/playground-mu-plugin/playground-includes/wp_http_dummy.php b/packages/playground/blueprints/src/lib/playground-mu-plugin/playground-includes/wp_http_dummy.php similarity index 100% rename from packages/playground/remote/src/lib/playground-mu-plugin/playground-includes/wp_http_dummy.php rename to packages/playground/blueprints/src/lib/playground-mu-plugin/playground-includes/wp_http_dummy.php diff --git a/packages/playground/remote/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php b/packages/playground/blueprints/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php similarity index 100% rename from packages/playground/remote/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php rename to packages/playground/blueprints/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php diff --git a/packages/playground/blueprints/src/lib/setup-mu-plugins.ts b/packages/playground/blueprints/src/lib/setup-mu-plugins.ts new file mode 100644 index 00000000000..a8bf1065c08 --- /dev/null +++ b/packages/playground/blueprints/src/lib/setup-mu-plugins.ts @@ -0,0 +1,47 @@ +import { joinPaths } from '@php-wasm/util'; + +/** @ts-ignore */ +import transportFetch from './playground-mu-plugin/playground-includes/wp_http_fetch.php?raw'; +/** @ts-ignore */ +import transportDummy from './playground-mu-plugin/playground-includes/wp_http_dummy.php?raw'; +/** @ts-ignore */ +import playgroundMuPlugin from './playground-mu-plugin/0-playground.php?raw'; +import { UniversalPHP, writeFiles } from '@php-wasm/universal'; + +export async function installPlaygroundMuPlugin(php: UniversalPHP) { + const muPluginsPath = joinPaths( + await php.documentRoot, + 'wp-content/mu-plugins/' + ); + await writeFiles(php, muPluginsPath, { + '0-playground.php': playgroundMuPlugin, + 'playground-includes/wp_http_dummy.php': transportDummy, + 'playground-includes/wp_http_fetch.php': transportFetch, + }); +} + +export async function linkSqliteMuPlugin(php: UniversalPHP) { + const docroot = await php.documentRoot; + const muPluginsPath = joinPaths(docroot, 'wp-content/mu-plugins/'); + const sqlitePluginPath = joinPaths( + muPluginsPath, + 'sqlite-database-integration' + ); + await php.mkdir(sqlitePluginPath); + await php.writeFile( + joinPaths(muPluginsPath, '0-sqlite.php'), + ' { * `wp-content` and `wp-includes` directories, they will replace * the corresponding directories in Playground's documentRoot. * - * Any files that Playground recognizes as "excluded from the export" + * Any files that Playground recognizes as "exceptd from the export" * will carry over from the existing document root into the imported * directories. For example, the sqlite-database-integration plugin. * - * @param playground Playground client. + * @param php Playground client. * @param wordPressFilesZip Zipped WordPress site. */ export const importWordPressFiles: StepHandler< ImportWordPressFilesStep -> = async (playground, { wordPressFilesZip, pathInZip = '' }) => { - const documentRoot = await playground.documentRoot; - - // Unzip - let importPath = joinPaths('/tmp', 'import'); - await playground.mkdir(importPath); - await unzip(playground, { - zipFile: wordPressFilesZip, - extractToPath: importPath, +> = async (php, { wordPressFilesZip, pathInZip = '' }) => { + await setSnapshot(php, wordPressFilesZip, pathInZip); + await linkSnapshot(php); +}; + +export async function setSnapshot( + php: UniversalPHP, + snapshotZip: File, + pathInZip = '' +) { + const documentRoot = await php.documentRoot; + // Extract the snapshot to a temporary directory. + // Use a subdirectory of the document root to avoid a slow recursive + // copy operation across Emscripten filesystems – in Node.js, + // the /tmp directory is likely kept in MEMFS, while the document + // root is typically a NODEFS mount. + const snapshotPath = joinPaths(documentRoot, '.snapshot'); + await unzipSnapshot(php, snapshotZip, pathInZip, snapshotPath); + await backfillWordPressCore(php, snapshotPath, documentRoot); + await backfillWpConfig(php, snapshotPath, documentRoot); + await backfillSqliteMuPlugin(php, snapshotPath, documentRoot); + + // Remove previous core files + for (const file of await php.listFiles(documentRoot)) { + if (file === '.snapshot') { + continue; + } + await removePath(php, joinPaths(documentRoot, file)); + } + await moveContents(php, snapshotPath, documentRoot); + await removePath(php, joinPaths(documentRoot, '.snapshot')); +} + +async function unzipSnapshot( + php: UniversalPHP, + snapshotZip: File, + pathInZip = '', + targetPath: string +) { + await unzip(php, { + zipFile: snapshotZip, + extractToPath: targetPath, }); - importPath = joinPaths(importPath, pathInZip); - - // Carry over any Playground-related files, such as the - // SQLite database plugin, from the current wp-content - // into the one that's about to be imported - const importedWpContentPath = joinPaths(importPath, 'wp-content'); - const wpContentPath = joinPaths(documentRoot, 'wp-content'); - for (const relativePath of wpContentFilesExcludedFromExport) { - // Remove any paths that were supposed to be excluded from the export - // but maybe weren't - const excludedImportPath = joinPaths( - importedWpContentPath, - relativePath + + let importedFilesPath = targetPath; + if (pathInZip) { + // If pathInZip is explicitly provided, use it. + importedFilesPath = joinPaths(targetPath, pathInZip); + } else if ( + (await php.fileExists(joinPaths(targetPath, 'wordpress'))) && + !(await php.fileExists(joinPaths(targetPath, 'wp-content'))) + ) { + // If importing a WordPress.org zip, it may contain a top-level + // directory named "wordpress". If so, use that as the import path. + importedFilesPath = joinPaths(importedFilesPath, 'wordpress'); + } + + // If the imported files were in a nested directory in the zip, + // move them over to the document root. + if (importedFilesPath !== targetPath) { + await moveContents(php, importedFilesPath, targetPath); + } +} + +async function backfillWordPressCore( + php: UniversalPHP, + snapshotPath: string, + previousCorePath: string +) { + const importedFiles = await php.listFiles(snapshotPath); + const snapshotType = detectSnapshotType(importedFiles); + if (snapshotType === 'unknown') { + throw new Error( + 'WordPress snapshot must contain either the full WordPress core or just wp-config.php and the wp-content directory.' ); - await removePath(playground, excludedImportPath); + } + + if (snapshotType === 'wp-core') { + return; + } - // Replace them with files sourced from the live wp-content directory - const restoreFromPath = joinPaths(wpContentPath, relativePath); - if (await playground.fileExists(restoreFromPath)) { - await playground.mkdir(dirname(excludedImportPath)); - await playground.mv(restoreFromPath, excludedImportPath); + if (snapshotType === 'wp-content') { + // Bring over WordPress core files from the previous core path + await moveContents(php, previousCorePath, snapshotPath, { + except: ['wp-content', 'wp-config.php', basename(snapshotPath)], + }); + return; + } + + // @TODO: Download the latest minified WordPress zip and source the files from there. + // @TODO: Include a Blueprint/manifest with the Playground export, and use it to source + // the expected WordPress version. + throw new Error( + 'Cannot initialize Playground with just the wp-content directory without loading WordPress core first. ' + + 'Most likely you specified a zip file URL as a preferred WordPress version. ' + + 'Either provide a zip file that contains the top-level WordPress files and directories, or ' + + 'import your zip by explicitly listing the "importWordPressFiles" in the Blueprint.' + ); +} + +/** + * Ensure the SQLite integration plugin is installed. + * The prebuilt Playground WordPress zips include it, + * but this worker may also be initialized with a custom zip + * or an OPFS directory handle. + * + * The same logic is present in packages/playground/wordpress/build/Dockerfile + * be sure to keep it in sync. + */ +export async function backfillSqliteMuPlugin( + php: UniversalPHP, + snapshotPath: string, + previousCorePath?: string +) { + // The SQLite plugin used to be a regular plugin in old Playground exports. + // Let's be nice and clean it up if it's present. + const sqlitePluginPath = joinPaths( + snapshotPath, + 'wp-content/plugins/sqlite-database-integration' + ); + await removePath(php, sqlitePluginPath); + + await php.mkdir(joinPaths(snapshotPath, 'wp-content', 'mu-plugins')); + const sqliteMuPluginPath = 'wp-content/plugins/sqlite-database-integration'; + if (await php.fileExists(joinPaths(snapshotPath, sqliteMuPluginPath))) { + // The SQLite plugin is present in the imported snapshot, we're good. + return false; + } + + if (previousCorePath) { + const prevMuPluginPath = joinPaths( + previousCorePath, + sqliteMuPluginPath + ); + if (await php.fileExists(prevMuPluginPath)) { + // The SQLite plugin was present in the previous core, let's carry it over. + await moveContents(php, prevMuPluginPath, sqliteMuPluginPath, { + except: ['sqlite-database-integration'], + }); + return true; } } - // Carry over the database directory if the imported zip file doesn't - // already contain one. - const importedDatabasePath = joinPaths( - importPath, - 'wp-content', - 'database' + // Otherwise, let's download and install the SQLite plugin + const muPluginsPath = joinPaths(snapshotPath, 'wp-content/mu-plugins/'); + const plugin = await fetch( + 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip' ); - if (!(await playground.fileExists(importedDatabasePath))) { - await playground.mv( - joinPaths(documentRoot, 'wp-content', 'database'), - importedDatabasePath + await unzip(php, { + // The zip file contains a directory with the same name as the plugin. + extractToPath: muPluginsPath, + zipFile: new File( + [await plugin.blob()], + 'sqlite-database-integration.latest.zip' + ), + }); + + return true; +} + +function detectSnapshotType(files: string[]) { + if (files.includes('wp-includes') && files.includes('wp-admin')) { + return 'wp-core' as const; + } + + if ( + (files.length === 1 && files.includes('wp-content')) || + (files.length === 2 && + files.includes('wp-content') && + files.includes('wp-config.php')) + ) { + return 'wp-content' as const; + } + + return 'unknown' as const; +} + +async function backfillWpConfig( + php: UniversalPHP, + snapshotPath: string, + previousCorePath?: string +) { + // Vanilla WordPress core has no wp-config.php. Let's backfill it + // using the bundled wp-config-sample.php. + if (await php.fileExists(joinPaths(snapshotPath, 'wp-config.php'))) { + return; + } + const candidates = [joinPaths(snapshotPath, 'wp-config-sample.php')]; + if (previousCorePath) { + candidates.push( + joinPaths(previousCorePath, 'wp-config-sample.php'), + joinPaths(previousCorePath, 'wp-config.php') ); } + for (const candidate of candidates) { + if (await php.fileExists(candidate)) { + await php.mv(candidate, joinPaths(snapshotPath, 'wp-config.php')); + return; + } + } + throw new Error( + 'Neither wp-config.php nor wp-config-sample.php found in the imported WordPress snapshot' + ); +} - // Move all the paths from the imported directory into the document root. - // Overwrite, if needed. - const importedFilenames = await playground.listFiles(importPath); - for (const fileName of importedFilenames) { - await removePath(playground, joinPaths(documentRoot, fileName)); - await playground.mv( - joinPaths(importPath, fileName), - joinPaths(documentRoot, fileName) +async function linkWpConfig(php: UniversalPHP) { + const snapshotPath = await php.documentRoot; + // Vanilla WordPress core has no wp-config.php. Let's backfill it + // using the bundled wp-config-sample.php. + if (await php.fileExists(joinPaths(snapshotPath, 'wp-config.php'))) { + return; + } + const wpConfigSample = joinPaths(snapshotPath, 'wp-config-sample.php'); + if (!(await php.fileExists(wpConfigSample))) { + throw new Error( + 'Neither wp-config.php nor wp-config-sample.php found in the imported WordPress snapshot' ); } + await php.mv(wpConfigSample, joinPaths(snapshotPath, 'wp-config.php')); +} + +async function moveContents( + php: UniversalPHP, + from: string, + to: string, + { except = [] }: { except?: string[] } = {} +): Promise { + await php.mkdir(to); + const files = await php.listFiles(from); + for (const file of files) { + if (except.includes(file)) { + continue; + } + await php.mv(joinPaths(from, file), joinPaths(to, file)); + } +} + +/** + * Private API. + * + * Turns a static WordPress snapshot present in the document root + * into a configured, runnable WordPress site. + * + * @private + * @param php + */ +export async function linkSnapshot(php: UniversalPHP) { + const documentRoot = await php.documentRoot; - // Remove the directory where we unzipped the imported zip file. - await playground.rmdir(importPath); + await linkWpConfig(php); + await linkSqliteMuPlugin(php); + + // Ensure the Playground mu-plugin is present if we're running in the + // browser. This will overwrite the existing Playground mu-plugin if + // it's present, but that's fine. We always want to run the latest version. + if (currentJsRuntime === 'WEB' || currentJsRuntime === 'WORKER') { + await installPlaygroundMuPlugin(php); + } // Adjust the site URL - await defineSiteUrl(playground, { - siteUrl: await playground.absoluteUrl, + await defineSiteUrl(php, { + siteUrl: await php.absoluteUrl, }); - // Upgrade the database - const upgradePhp = phpVar( - joinPaths(documentRoot, 'wp-admin', 'upgrade.php') - ); - await playground.run({ - throwOnError: true, - code: `?'; + let result = ''; + for (let i = length; i > 0; --i) + result += chars[Math.floor(Math.random() * chars.length)]; + return result; +} diff --git a/packages/playground/blueprints/src/lib/steps/run-wp-installation-wizard.ts b/packages/playground/blueprints/src/lib/steps/run-wp-installation-wizard.ts index fd803b50dbb..4e25c88f2ec 100644 --- a/packages/playground/blueprints/src/lib/steps/run-wp-installation-wizard.ts +++ b/packages/playground/blueprints/src/lib/steps/run-wp-installation-wizard.ts @@ -5,7 +5,7 @@ import { StepHandler } from '.'; */ export interface RunWpInstallationWizardStep { step: 'runWpInstallationWizard'; - options: WordPressInstallationOptions; + options?: WordPressInstallationOptions; } export interface WordPressInstallationOptions { @@ -21,7 +21,7 @@ export interface WordPressInstallationOptions { */ export const runWpInstallationWizard: StepHandler< RunWpInstallationWizardStep -> = async (playground, { options }) => { +> = async (playground, { options = {} }) => { await playground.request({ url: '/wp-admin/install.php?step=2', method: 'POST', diff --git a/packages/playground/blueprints/tsconfig.lib.json b/packages/playground/blueprints/tsconfig.lib.json index 829b0bc14c8..c341779b2e0 100644 --- a/packages/playground/blueprints/tsconfig.lib.json +++ b/packages/playground/blueprints/tsconfig.lib.json @@ -5,6 +5,6 @@ "declaration": true, "types": ["node"] }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "src/lib/setup-mu-plugins.ts"], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] } diff --git a/packages/playground/remote/src/lib/utils.ts b/packages/playground/remote/src/lib/utils.ts deleted file mode 100644 index ec870f6d84b..00000000000 --- a/packages/playground/remote/src/lib/utils.ts +++ /dev/null @@ -1,8 +0,0 @@ -export function randomString(length: number) { - const chars = - '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!@#$%^&*()_+=-[]/.,<>?'; - let result = ''; - for (let i = length; i > 0; --i) - result += chars[Math.floor(Math.random() * chars.length)]; - return result; -} diff --git a/packages/playground/remote/src/lib/worker-thread.ts b/packages/playground/remote/src/lib/worker-thread.ts index 5740db87800..78c8384ab20 100644 --- a/packages/playground/remote/src/lib/worker-thread.ts +++ b/packages/playground/remote/src/lib/worker-thread.ts @@ -13,7 +13,6 @@ import { SupportedPHPVersion, SupportedPHPVersionsList, rotatePHPRuntime, - writeFiles, } from '@php-wasm/universal'; import { createSpawnHandler } from '@php-wasm/util'; import { @@ -28,18 +27,12 @@ import { } from './opfs/bind-opfs'; import { defineSiteUrl, - defineWpConfigConsts, - unzip, + backfillSqliteMuPlugin, + linkSnapshot, + setSnapshot, } from '@wp-playground/blueprints'; -/** @ts-ignore */ -import transportFetch from './playground-mu-plugin/playground-includes/wp_http_fetch.php?raw'; -/** @ts-ignore */ -import transportDummy from './playground-mu-plugin/playground-includes/wp_http_dummy.php?raw'; -/** @ts-ignore */ -import playgroundMuPlugin from './playground-mu-plugin/0-playground.php?raw'; import { joinPaths } from '@php-wasm/util'; -import { randomString } from './utils'; // post message to parent self.postMessage('worker-script-started'); @@ -243,39 +236,15 @@ try { // If WordPress isn't already installed, download and extract it from // the zip file. if (!wordPressAvailableInOPFS) { - await unzip(php, { - zipFile: new File( - [await (await wordPressRequest!).blob()], - 'wp.zip' - ), - extractToPath: DOCROOT, - }); - - // Randomize the WordPress secrets - await defineWpConfigConsts(php, { - consts: { - 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 snapshot = new File( + [await (await wordPressRequest!).blob()], + 'wp.zip' + ); + await setSnapshot(php, snapshot); } - // Always install the playground mu-plugin, even if WordPress is loaded - // from the OPFS. This ensures: - // * The mu-plugin is always there, even when a custom WordPress directory - // is mounted. - // * The mu-plugin is always up to date. - await writeFiles(php, joinPaths(docroot, '/wp-content/mu-plugins'), { - '0-playground.php': playgroundMuPlugin, - 'playground-includes/wp_http_dummy.php': transportDummy, - 'playground-includes/wp_http_fetch.php': transportFetch, - }); + await backfillSqliteMuPlugin(php, docroot); + await linkSnapshot(php); if (virtualOpfsDir) { await bindOpfs({ diff --git a/packages/playground/remote/tsconfig.lib.json b/packages/playground/remote/tsconfig.lib.json index 62d0b9580f3..8de6072eef5 100644 --- a/packages/playground/remote/tsconfig.lib.json +++ b/packages/playground/remote/tsconfig.lib.json @@ -9,6 +9,10 @@ "@nx/react/typings/image.d.ts" ] }, - "include": ["src/**/*.ts", "service-worker.ts"], + "include": [ + "src/**/*.ts", + "service-worker.ts", + "../blueprints/src/lib/setup-mu-plugins.ts" + ], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] } diff --git a/packages/playground/wordpress/build/Dockerfile b/packages/playground/wordpress/build/Dockerfile index e682148c58d..cb8816e44b0 100644 --- a/packages/playground/wordpress/build/Dockerfile +++ b/packages/playground/wordpress/build/Dockerfile @@ -26,6 +26,8 @@ RUN mkdir wordpress/wp-content/mu-plugins # * Ensure it won't accidentally be disabled by the user # * Prevent it from blocking a multisite setup where disabling all the plugins is required # https://github.com/WordPress/sqlite-database-integration +# The same logic is present in packages/playground/remote/src/lib/worker-thread.ts, +# be sure to keep it in sync. RUN git clone https://github.com/WordPress/sqlite-database-integration.git \ wordpress/wp-content/mu-plugins/sqlite-database-integration \ --branch main \ diff --git a/packages/playground/wordpress/tsconfig.lib.json b/packages/playground/wordpress/tsconfig.lib.json index 672b0253c97..fc3aed667d6 100644 --- a/packages/playground/wordpress/tsconfig.lib.json +++ b/packages/playground/wordpress/tsconfig.lib.json @@ -5,6 +5,6 @@ "declaration": true, "types": ["node", "vite/client"] }, - "include": ["src/**/*.ts"], + "include": ["src/**/*.ts", "../blueprints/src/lib/setup-mu-plugins.ts"], "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] } From 8d8105b359ac283c527bd788b5c471613461ffbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 6 Feb 2024 23:23:42 +0100 Subject: [PATCH 2/6] More cohesive set and link phases --- packages/playground/blueprints/src/index.ts | 9 +- .../blueprints/src/lib/setup-mu-plugins.ts | 50 ++++- .../src/lib/steps/import-wordpress-files.ts | 189 ++++++------------ .../remote/src/lib/worker-thread.ts | 2 - 4 files changed, 111 insertions(+), 139 deletions(-) diff --git a/packages/playground/blueprints/src/index.ts b/packages/playground/blueprints/src/index.ts index 2b203cc7363..d1e7e1e290e 100644 --- a/packages/playground/blueprints/src/index.ts +++ b/packages/playground/blueprints/src/index.ts @@ -3,12 +3,11 @@ import '@php-wasm/node-polyfills'; export * from './lib/steps'; export * from './lib/steps/handlers'; +export { linkSnapshot, setSnapshot } from './lib/steps/import-wordpress-files'; export { - linkSnapshot, - setSnapshot, - backfillSqliteMuPlugin, -} from './lib/steps/import-wordpress-files'; -export { installPlaygroundMuPlugin } from './lib/setup-mu-plugins'; + installPlaygroundMuPlugin, + installSqliteMuPlugin, +} from './lib/setup-mu-plugins'; export { runBlueprintSteps, compileBlueprint } from './lib/compile'; export type { Blueprint } from './lib/blueprint'; export type { diff --git a/packages/playground/blueprints/src/lib/setup-mu-plugins.ts b/packages/playground/blueprints/src/lib/setup-mu-plugins.ts index a8bf1065c08..2b375e1f7e0 100644 --- a/packages/playground/blueprints/src/lib/setup-mu-plugins.ts +++ b/packages/playground/blueprints/src/lib/setup-mu-plugins.ts @@ -7,6 +7,7 @@ import transportDummy from './playground-mu-plugin/playground-includes/wp_http_d /** @ts-ignore */ import playgroundMuPlugin from './playground-mu-plugin/0-playground.php?raw'; import { UniversalPHP, writeFiles } from '@php-wasm/universal'; +import { unzip } from './steps/unzip'; export async function installPlaygroundMuPlugin(php: UniversalPHP) { const muPluginsPath = joinPaths( @@ -20,13 +21,60 @@ export async function installPlaygroundMuPlugin(php: UniversalPHP) { }); } -export async function linkSqliteMuPlugin(php: UniversalPHP) { +export async function installSqliteMuPlugin( + php: UniversalPHP, + snapshotPath: string +) { + await ensureSqliteMuPlugin(php, snapshotPath); + await activateSqliteMuPlugin(php); +} + +/** + * Ensure the SQLite integration plugin is installed. + * The prebuilt Playground WordPress zips include it, + * but this worker may also be initialized with a custom zip + * or an OPFS directory handle. + * + * The same logic is present in packages/playground/wordpress/build/Dockerfile + * be sure to keep it in sync. + */ +async function ensureSqliteMuPlugin(php: UniversalPHP, snapshotPath: string) { + const sqliteMuPluginPath = 'wp-content/plugins/sqlite-database-integration'; + if (await php.fileExists(joinPaths(snapshotPath, sqliteMuPluginPath))) { + // The SQLite plugin is present in the imported snapshot, we're good. + return false; + } + + // Otherwise, let's download and install the SQLite plugin + const muPluginsPath = joinPaths(snapshotPath, 'wp-content/mu-plugins/'); + const plugin = await fetch( + 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip' + ); + await unzip(php, { + // The zip file contains a directory with the same name as the plugin. + extractToPath: muPluginsPath, + zipFile: new File( + [await plugin.blob()], + 'sqlite-database-integration.latest.zip' + ), + }); + + return true; +} + +export async function activateSqliteMuPlugin(php: UniversalPHP) { const docroot = await php.documentRoot; const muPluginsPath = joinPaths(docroot, 'wp-content/mu-plugins/'); const sqlitePluginPath = joinPaths( muPluginsPath, 'sqlite-database-integration' ); + if (!(await php.fileExists(sqlitePluginPath))) { + console.log( + 'sqlite-database-integration not found in mu-plugins, skipping' + ); + return; + } await php.mkdir(sqlitePluginPath); await php.writeFile( joinPaths(muPluginsPath, '0-sqlite.php'), diff --git a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts index 14fcdcdd468..483f0a85abc 100644 --- a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts +++ b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts @@ -5,7 +5,7 @@ import { UniversalPHP, currentJsRuntime } from '@php-wasm/universal'; import { defineSiteUrl } from './define-site-url'; import { installPlaygroundMuPlugin, - linkSqliteMuPlugin, + installSqliteMuPlugin, } from '../setup-mu-plugins'; import { runWpInstallationWizard } from './run-wp-installation-wizard'; import { defineWpConfigConsts } from './define-wp-config-consts'; @@ -71,18 +71,21 @@ export async function setSnapshot( const snapshotPath = joinPaths(documentRoot, '.snapshot'); await unzipSnapshot(php, snapshotZip, pathInZip, snapshotPath); await backfillWordPressCore(php, snapshotPath, documentRoot); - await backfillWpConfig(php, snapshotPath, documentRoot); - await backfillSqliteMuPlugin(php, snapshotPath, documentRoot); + await moveSnapshotToDocroot(php, snapshotPath); + await removePath(php, snapshotPath); +} - // Remove previous core files +async function moveSnapshotToDocroot(php: UniversalPHP, snapshotPath: string) { + const documentRoot = await php.documentRoot; + // Remove the old snapshot files for (const file of await php.listFiles(documentRoot)) { if (file === '.snapshot') { continue; } await removePath(php, joinPaths(documentRoot, file)); } + // Move the new snapshot files to the document root await moveContents(php, snapshotPath, documentRoot); - await removePath(php, joinPaths(documentRoot, '.snapshot')); } async function unzipSnapshot( @@ -96,6 +99,7 @@ async function unzipSnapshot( extractToPath: targetPath, }); + // Find WordPress core files in the extracted snapshot. let importedFilesPath = targetPath; if (pathInZip) { // If pathInZip is explicitly provided, use it. @@ -114,6 +118,34 @@ async function unzipSnapshot( if (importedFilesPath !== targetPath) { await moveContents(php, importedFilesPath, targetPath); } + + // Ensure wp-config.php is present in the extracted snapshot. + if (!(await php.fileExists(joinPaths(targetPath, 'wp-config.php')))) { + const samplePath = joinPaths(targetPath, 'wp-config-sample.php'); + const wpConfig = (await php.fileExists(samplePath)) + ? await php.readFileAsText(samplePath) + : ` Date: Tue, 6 Feb 2024 23:25:24 +0100 Subject: [PATCH 3/6] Get rid of the promoteSnapshot function --- .../src/lib/steps/import-wordpress-files.ts | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts index 483f0a85abc..e30b14498c7 100644 --- a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts +++ b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts @@ -71,21 +71,9 @@ export async function setSnapshot( const snapshotPath = joinPaths(documentRoot, '.snapshot'); await unzipSnapshot(php, snapshotZip, pathInZip, snapshotPath); await backfillWordPressCore(php, snapshotPath, documentRoot); - await moveSnapshotToDocroot(php, snapshotPath); - await removePath(php, snapshotPath); -} - -async function moveSnapshotToDocroot(php: UniversalPHP, snapshotPath: string) { - const documentRoot = await php.documentRoot; - // Remove the old snapshot files - for (const file of await php.listFiles(documentRoot)) { - if (file === '.snapshot') { - continue; - } - await removePath(php, joinPaths(documentRoot, file)); - } - // Move the new snapshot files to the document root + await removeContents(php, documentRoot, { except: ['.snapshot'] }); await moveContents(php, snapshotPath, documentRoot); + await removePath(php, snapshotPath); } async function unzipSnapshot( @@ -218,6 +206,19 @@ async function moveContents( await php.mv(joinPaths(from, file), joinPaths(to, file)); } } +async function removeContents( + php: UniversalPHP, + from: string, + { except = [] }: { except?: string[] } = {} +): Promise { + const files = await php.listFiles(from); + for (const file of files) { + if (except.includes(file)) { + continue; + } + await removePath(php, joinPaths(from, file)); + } +} /** * Private API. From 9bee5f4d8680b9202ba35d24444b2d10c157b346 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 6 Feb 2024 23:27:53 +0100 Subject: [PATCH 4/6] Simplify the snapshot type detection --- .../blueprints/src/lib/steps/import-wordpress-files.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts index e30b14498c7..90508792ee5 100644 --- a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts +++ b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts @@ -180,10 +180,9 @@ function detectSnapshotType(files: string[]) { } if ( - (files.length === 1 && files.includes('wp-content')) || - (files.length === 2 && - files.includes('wp-content') && - files.includes('wp-config.php')) + files.length === 2 && + files.includes('wp-content') && + files.includes('wp-config.php') ) { return 'wp-content' as const; } From 58111979608fe5d3adc224e58c193de938534e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Wed, 7 Feb 2024 15:08:26 +0100 Subject: [PATCH 5/6] Keep exploring --- .../src/lib/steps/import-wordpress-files.ts | 52 ++++++++++--------- .../playground/wordpress/build/Dockerfile | 8 +-- 2 files changed, 32 insertions(+), 28 deletions(-) diff --git a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts index 90508792ee5..ba80c922ae5 100644 --- a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts +++ b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts @@ -74,6 +74,11 @@ export async function setSnapshot( await removeContents(php, documentRoot, { except: ['.snapshot'] }); await moveContents(php, snapshotPath, documentRoot); await removePath(php, snapshotPath); + await defineWpConfigConsts(php, { + consts: { + CONCATENATE_SCRIPTS: false, + }, + }); } async function unzipSnapshot( @@ -120,6 +125,20 @@ async function unzipSnapshot( } require_once ABSPATH . 'wp-settings.php';`; await php.writeFile(joinPaths(targetPath, 'wp-config.php'), wpConfig); + + // Randomize the WordPress secrets in the freshly created wp-config.php. + await defineWpConfigConsts(php, { + consts: { + 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), + }, + }); } // Ensure the mu-plugins directory is present in the extracted snapshot. @@ -229,41 +248,26 @@ async function removeContents( * @param php */ export async function linkSnapshot(php: UniversalPHP) { - const documentRoot = await php.documentRoot; - // Enforce the required Playground and SQLite mu-plugins in // the browser. if (currentJsRuntime === 'WEB' || currentJsRuntime === 'WORKER') { + const documentRoot = await php.documentRoot; await installSqliteMuPlugin(php, documentRoot); await installPlaygroundMuPlugin(php); + + // Run the installation wizard if the database is missing + const dbExists = await php.fileExists( + joinPaths(documentRoot, 'wp-content', 'database', '.ht.sqlite') + ); + if (!dbExists) { + await runWpInstallationWizard(php, {}); + } } // Adjust the site URL await defineSiteUrl(php, { siteUrl: await php.absoluteUrl, }); - - // Randomize the WordPress secrets - await defineWpConfigConsts(php, { - consts: { - 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), - }, - }); - - // Run the installation wizard if the database is missing - const dbExists = await php.fileExists( - joinPaths(documentRoot, 'wp-content', 'database', '.ht.sqlite') - ); - if (!dbExists) { - await runWpInstallationWizard(php, {}); - } } async function removePath(playground: UniversalPHP, path: string) { diff --git a/packages/playground/wordpress/build/Dockerfile b/packages/playground/wordpress/build/Dockerfile index cb8816e44b0..019d735a273 100644 --- a/packages/playground/wordpress/build/Dockerfile +++ b/packages/playground/wordpress/build/Dockerfile @@ -184,10 +184,10 @@ COPY --from=php /root/wordpress ./wordpress # === Postprocess WordPress === -# Disable load-scripts.php -RUN cd wordpress && \ - sed "s/ wp-config.php.new && \ - mv wp-config.php.new wp-config.php +# Remove the wp-config.php file now that WordPress +# is installed and let Playground generate a new one +# on load. +RUN rm wordpress/wp-config.php # Build the final wp.zip file RUN mv wordpress /wordpress && \ From 21ccd612e09e7ea8ba87c1a0a3bdc338c131cca8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Fri, 9 Feb 2024 16:15:36 +0100 Subject: [PATCH 6/6] Assume empty filesystem when unzipping snapshots --- .../php-wasm/universal/src/lib/base-php.ts | 15 +- .../universal/src/lib/universal-php.ts | 5 + .../php-wasm/universal/src/lib/write-files.ts | 15 +- packages/playground/blueprints/src/index.ts | 4 - .../src/lib/steps/import-wordpress-files.ts | 13 +- .../blueprints/src/lib/steps/unzip.ts | 36 +--- .../src/lib/steps/zip-wp-content.ts | 18 +- .../lib/utils/run-php-with-zip-functions.ts | 12 -- packages/playground/php-bridge/.eslintrc.json | 32 ++++ packages/playground/php-bridge/README.md | 11 ++ packages/playground/php-bridge/package.json | 5 + packages/playground/php-bridge/project.json | 34 ++++ .../playground/php-bridge/src/index.spec.ts | 27 +++ packages/playground/php-bridge/src/index.ts | 75 +++++++++ .../src/lib/playground-php-bridge.spec.ts | 7 + .../src/lib/playground-php-bridge.ts | 3 + .../src/playground-library.php} | 31 +++- packages/playground/php-bridge/tsconfig.json | 22 +++ .../playground/php-bridge/tsconfig.lib.json | 10 ++ .../playground/php-bridge/tsconfig.spec.json | 25 +++ packages/playground/php-bridge/vite.config.ts | 48 ++++++ .../playground-mu-plugin/0-playground.php | 0 .../playground-includes/wp_http_dummy.php | 0 .../playground-includes/wp_http_fetch.php | 0 .../src/lib/worker-libs}/setup-mu-plugins.ts | 30 ++-- .../src/lib/worker-libs/setup-snapshot.ts | 154 ++++++++++++++++++ .../remote/src/lib/worker-thread.ts | 63 ++++--- packages/playground/remote/tsconfig.lib.json | 2 +- tsconfig.base.json | 3 + 29 files changed, 594 insertions(+), 106 deletions(-) delete mode 100644 packages/playground/blueprints/src/lib/utils/run-php-with-zip-functions.ts create mode 100644 packages/playground/php-bridge/.eslintrc.json create mode 100644 packages/playground/php-bridge/README.md create mode 100644 packages/playground/php-bridge/package.json create mode 100644 packages/playground/php-bridge/project.json create mode 100644 packages/playground/php-bridge/src/index.spec.ts create mode 100644 packages/playground/php-bridge/src/index.ts create mode 100644 packages/playground/php-bridge/src/lib/playground-php-bridge.spec.ts create mode 100644 packages/playground/php-bridge/src/lib/playground-php-bridge.ts rename packages/playground/{blueprints/src/lib/utils/zip-functions.php => php-bridge/src/playground-library.php} (71%) create mode 100644 packages/playground/php-bridge/tsconfig.json create mode 100644 packages/playground/php-bridge/tsconfig.lib.json create mode 100644 packages/playground/php-bridge/tsconfig.spec.json create mode 100644 packages/playground/php-bridge/vite.config.ts rename packages/playground/{blueprints/src/lib => remote/src/lib/worker-libs}/playground-mu-plugin/0-playground.php (100%) rename packages/playground/{blueprints/src/lib => remote/src/lib/worker-libs}/playground-mu-plugin/playground-includes/wp_http_dummy.php (100%) rename packages/playground/{blueprints/src/lib => remote/src/lib/worker-libs}/playground-mu-plugin/playground-includes/wp_http_fetch.php (100%) rename packages/playground/{blueprints/src/lib => remote/src/lib/worker-libs}/setup-mu-plugins.ts (82%) create mode 100644 packages/playground/remote/src/lib/worker-libs/setup-snapshot.ts diff --git a/packages/php-wasm/universal/src/lib/base-php.ts b/packages/php-wasm/universal/src/lib/base-php.ts index a9ea3ba0af0..17ea3189924 100644 --- a/packages/php-wasm/universal/src/lib/base-php.ts +++ b/packages/php-wasm/universal/src/lib/base-php.ts @@ -28,7 +28,12 @@ import { improveWASMErrorReporting, UnhandledRejectionsTarget, } from './wasm-error-reporting'; -import { Semaphore, createSpawnHandler, joinPaths } from '@php-wasm/util'; +import { + Semaphore, + createSpawnHandler, + joinPaths, + phpVars, +} from '@php-wasm/util'; const STRING = 'string'; const NUMBER = 'number'; @@ -267,7 +272,13 @@ export abstract class BasePHP implements IsomorphicLocalPHP { } } if (typeof request.code === 'string') { - this.#setPHPCode(' ?>' + request.code); + const js = phpVars(request.variables || {}); + const phpVariablesDeclarations = Object.entries(js) + .map(([key, value]) => `$${key} = ${value};`) + .join('\n'); + this.#setPHPCode( + ` ${phpVariablesDeclarations} ?>${request.code}` + ); } this.#addServerGlobalEntriesInWasm(); const response = await this.#handleRequest(); diff --git a/packages/php-wasm/universal/src/lib/universal-php.ts b/packages/php-wasm/universal/src/lib/universal-php.ts index 299878dd977..3c6ef962150 100644 --- a/packages/php-wasm/universal/src/lib/universal-php.ts +++ b/packages/php-wasm/universal/src/lib/universal-php.ts @@ -543,6 +543,11 @@ export interface PHPRunOptions { */ code?: string; + /** + * The variables to pass to the PHP code. + */ + variables?: Record; + /** * Whether to throw an error if the PHP process exits with a non-zero code * or outputs to stderr. diff --git a/packages/php-wasm/universal/src/lib/write-files.ts b/packages/php-wasm/universal/src/lib/write-files.ts index ff0d8a0162c..5810eac3a9f 100644 --- a/packages/php-wasm/universal/src/lib/write-files.ts +++ b/packages/php-wasm/universal/src/lib/write-files.ts @@ -29,7 +29,7 @@ export interface WriteFilesOptions { export async function writeFiles( php: UniversalPHP, root: string, - newFiles: Record, + newFiles: Record, { rmRoot = false }: WriteFilesOptions = {} ) { if (rmRoot) { @@ -37,11 +37,22 @@ export async function writeFiles( await php.rmdir(root, { recursive: true }); } } + await php.mkdir(root); for (const [relativePath, content] of Object.entries(newFiles)) { const filePath = joinPaths(root, relativePath); if (!(await php.fileExists(dirname(filePath)))) { await php.mkdir(dirname(filePath)); } - await php.writeFile(filePath, content); + const finalContent = + content instanceof File + ? new Uint8Array(await content.arrayBuffer()) + : content; + await php.writeFile(filePath, finalContent); } + + const paths: Record = {}; + for (const [relativePath] of Object.entries(newFiles)) { + paths[relativePath] = joinPaths(root, relativePath); + } + return paths; } diff --git a/packages/playground/blueprints/src/index.ts b/packages/playground/blueprints/src/index.ts index d1e7e1e290e..a326e1e703a 100644 --- a/packages/playground/blueprints/src/index.ts +++ b/packages/playground/blueprints/src/index.ts @@ -4,10 +4,6 @@ import '@php-wasm/node-polyfills'; export * from './lib/steps'; export * from './lib/steps/handlers'; export { linkSnapshot, setSnapshot } from './lib/steps/import-wordpress-files'; -export { - installPlaygroundMuPlugin, - installSqliteMuPlugin, -} from './lib/setup-mu-plugins'; export { runBlueprintSteps, compileBlueprint } from './lib/compile'; export type { Blueprint } from './lib/blueprint'; export type { diff --git a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts index ba80c922ae5..d00f8eab14d 100644 --- a/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts +++ b/packages/playground/blueprints/src/lib/steps/import-wordpress-files.ts @@ -3,10 +3,6 @@ import { unzip } from './unzip'; import { basename, joinPaths } from '@php-wasm/util'; import { UniversalPHP, currentJsRuntime } from '@php-wasm/universal'; import { defineSiteUrl } from './define-site-url'; -import { - installPlaygroundMuPlugin, - installSqliteMuPlugin, -} from '../setup-mu-plugins'; import { runWpInstallationWizard } from './run-wp-installation-wizard'; import { defineWpConfigConsts } from './define-wp-config-consts'; @@ -87,10 +83,7 @@ async function unzipSnapshot( pathInZip = '', targetPath: string ) { - await unzip(php, { - zipFile: snapshotZip, - extractToPath: targetPath, - }); + // await unzip(php, snapshotZip, targetPath); // Find WordPress core files in the extracted snapshot. let importedFilesPath = targetPath; @@ -252,8 +245,8 @@ export async function linkSnapshot(php: UniversalPHP) { // the browser. if (currentJsRuntime === 'WEB' || currentJsRuntime === 'WORKER') { const documentRoot = await php.documentRoot; - await installSqliteMuPlugin(php, documentRoot); - await installPlaygroundMuPlugin(php); + // await installSqliteMuPlugin(php, documentRoot); + // await installPlaygroundMuPlugin(php); // Run the installation wizard if the database is missing const dbExists = await php.fileExists( diff --git a/packages/playground/blueprints/src/lib/steps/unzip.ts b/packages/playground/blueprints/src/lib/steps/unzip.ts index 0858a2548f1..79d9b8e2f4d 100644 --- a/packages/playground/blueprints/src/lib/steps/unzip.ts +++ b/packages/playground/blueprints/src/lib/steps/unzip.ts @@ -1,6 +1,5 @@ -import { phpVars } from '@php-wasm/util'; import { StepHandler } from '.'; -import { runPhpWithZipFunctions } from '../utils/run-php-with-zip-functions'; +import { unzip as _unzip } from '@wp-playground/php-bridge'; /** * @inheritDoc unzip @@ -30,13 +29,11 @@ export interface UnzipStep { extractToPath: string; } -const tmpPath = '/tmp/file.zip'; - /** * Unzip a zip file. * * @param playground Playground client. - * @param zipPath The zip file to unzip. + * @param zipFile The zip file to unzip. * @param extractTo The directory to extract the zip file to. */ export const unzip: StepHandler> = async ( @@ -44,33 +41,18 @@ export const unzip: StepHandler> = async ( { zipFile, zipPath, extractToPath } ) => { if (zipPath) { - // Compatibility with the old Blueprints API - // @TODO: Remove the zipPath option in the next major version - await playground.writeFile( - tmpPath, - await playground.readFileAsBuffer(zipPath) + zipFile = new File( + [await playground.readFileAsBuffer(zipPath)], + 'file.zip' ); console.warn( `The "zipPath" option of the unzip() Blueprint step is deprecated and will be removed. ` + `Use "zipFile" instead.` ); - } else if (zipFile) { - await playground.writeFile( - tmpPath, - new Uint8Array(await zipFile.arrayBuffer()) - ); - } else { - throw new Error('Either zipPath or zipFile must be provided'); } - const js = phpVars({ - zipPath: tmpPath, - extractToPath, - }); - await runPhpWithZipFunctions( - playground, - `unzip(${js.zipPath}, ${js.extractToPath});` - ); - if (playground.fileExists(tmpPath)) { - await playground.unlink(tmpPath); + + if (!zipFile) { + throw new Error('Either zipPath or zipFile must be provided'); } + return await _unzip(playground, zipFile, extractToPath); }; diff --git a/packages/playground/blueprints/src/lib/steps/zip-wp-content.ts b/packages/playground/blueprints/src/lib/steps/zip-wp-content.ts index 9870ff76d79..1dc0bed3da8 100644 --- a/packages/playground/blueprints/src/lib/steps/zip-wp-content.ts +++ b/packages/playground/blueprints/src/lib/steps/zip-wp-content.ts @@ -1,7 +1,7 @@ import { joinPaths, phpVars } from '@php-wasm/util'; import { UniversalPHP } from '@php-wasm/universal'; import { wpContentFilesExcludedFromExport } from '../utils/wp-content-files-excluded-from-exports'; -import { runPhpWithZipFunctions } from '../utils/run-php-with-zip-functions'; +// import { runPhpWithZipFunctions } from '../utils/run-php-with-zip-functions'; interface ZipWpContentOptions { /** @@ -59,14 +59,14 @@ export const zipWpContent = async ( } : {}, }); - await runPhpWithZipFunctions( - playground, - `zipDir(${js.wpContentPath}, ${js.zipPath}, array( - 'exclude_paths' => ${js.exceptPaths}, - 'zip_root' => ${js.documentRoot}, - 'additional_paths' => ${js.additionalPaths} - ));` - ); + // await runPhpWithZipFunctions( + // playground, + // `zipDir(${js.wpContentPath}, ${js.zipPath}, array( + // 'exclude_paths' => ${js.exceptPaths}, + // 'zip_root' => ${js.documentRoot}, + // 'additional_paths' => ${js.additionalPaths} + // ));` + // ); const fileBuffer = await playground.readFileAsBuffer(zipPath); playground.unlink(zipPath); diff --git a/packages/playground/blueprints/src/lib/utils/run-php-with-zip-functions.ts b/packages/playground/blueprints/src/lib/utils/run-php-with-zip-functions.ts deleted file mode 100644 index 8323a2519f7..00000000000 --- a/packages/playground/blueprints/src/lib/utils/run-php-with-zip-functions.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { UniversalPHP } from '@php-wasm/universal'; -// @ts-ignore -import zipFunctions from './zip-functions.php?raw'; -export async function runPhpWithZipFunctions( - playground: UniversalPHP, - code: string -) { - return await playground.run({ - throwOnError: true, - code: zipFunctions + code, - }); -} diff --git a/packages/playground/php-bridge/.eslintrc.json b/packages/playground/php-bridge/.eslintrc.json new file mode 100644 index 00000000000..ee9ffa963c8 --- /dev/null +++ b/packages/playground/php-bridge/.eslintrc.json @@ -0,0 +1,32 @@ +{ + "extends": ["../../../.eslintrc.json"], + "ignorePatterns": ["!**/*"], + "overrides": [ + { + "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.ts", "*.tsx"], + "rules": {} + }, + { + "files": ["*.js", "*.jsx"], + "rules": {} + }, + { + "files": ["*.json"], + "parser": "jsonc-eslint-parser", + "rules": { + "@nx/dependency-checks": [ + "error", + { + "ignoredFiles": [ + "{projectRoot}/vite.config.{js,ts,mjs,mts}" + ] + } + ] + } + } + ] +} diff --git a/packages/playground/php-bridge/README.md b/packages/playground/php-bridge/README.md new file mode 100644 index 00000000000..244c77f179c --- /dev/null +++ b/packages/playground/php-bridge/README.md @@ -0,0 +1,11 @@ +# playground-php-bridge + +This library was generated with [Nx](https://nx.dev). + +## Building + +Run `nx build playground-php-bridge` to build the library. + +## Running unit tests + +Run `nx test playground-php-bridge` to execute the unit tests via [Jest](https://jestjs.io). diff --git a/packages/playground/php-bridge/package.json b/packages/playground/php-bridge/package.json new file mode 100644 index 00000000000..a045577b51f --- /dev/null +++ b/packages/playground/php-bridge/package.json @@ -0,0 +1,5 @@ +{ + "version": "0.0.1", + "type": "module", + "private": true +} diff --git a/packages/playground/php-bridge/project.json b/packages/playground/php-bridge/project.json new file mode 100644 index 00000000000..a4c24abc1df --- /dev/null +++ b/packages/playground/php-bridge/project.json @@ -0,0 +1,34 @@ +{ + "name": "playground-php-bridge", + "$schema": "../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/playground/php-bridge/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/vite:build", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/playground/php-bridge" + } + }, + "test": { + "executor": "@nx/vite:test", + "outputs": ["{options.reportsDirectory}"], + "options": { + "passWithNoTests": true, + "reportsDirectory": "../../../coverage/packages/playground/php-bridge" + } + }, + "lint": { + "executor": "@nx/linter:eslint", + "outputs": ["{options.outputFile}"], + "options": { + "lintFilePatterns": [ + "packages/playground/php-bridge/**/*.ts", + "packages/playground/php-bridge/package.json" + ] + } + } + }, + "tags": [] +} diff --git a/packages/playground/php-bridge/src/index.spec.ts b/packages/playground/php-bridge/src/index.spec.ts new file mode 100644 index 00000000000..bc54832550b --- /dev/null +++ b/packages/playground/php-bridge/src/index.spec.ts @@ -0,0 +1,27 @@ +import { NodePHP } from '@php-wasm/node'; +import { withPlaygroundLibrary } from './index'; + +const phpVersion = '8.0'; + +describe('withPlaygroundLibrary()', () => { + let php: NodePHP; + beforeEach(async () => { + php = await NodePHP.load(phpVersion as any, {}); + php.mkdir('/php'); + php.setPhpIniEntry('disable_functions', ''); + }); + it('Should provide the variables', async () => { + const result = await withPlaygroundLibrary( + php as any, + ` = {} +) { + const vars: Record = {}; + const files: Record = {}; + for (const [key, value] of Object.entries(inputs)) { + if (value instanceof File) { + files[key] = value; + } else { + vars[key] = value; + } + } + + const tmpRoot = `/tmp/${randomFilename(8)}`; + const tmpPaths = await writeFiles(php, tmpRoot, files); + + try { + await php.writeFile(`/tmp/playground-library.php`, playgroundLibrary); + return await php.run({ + throwOnError: true, + code: `${code}`, + variables: { ...vars, ...tmpPaths }, + }); + } finally { + await php.rmdir(tmpRoot); + } +} + +/** + * @TODO: Rebase this PH and import this from @php-wasm/utils + */ +export function randomFilename(length: number) { + const chars = + '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; + let result = ''; + for (let i = length; i > 0; --i) + result += chars[Math.floor(Math.random() * chars.length)]; + return result; +} diff --git a/packages/playground/php-bridge/src/lib/playground-php-bridge.spec.ts b/packages/playground/php-bridge/src/lib/playground-php-bridge.spec.ts new file mode 100644 index 00000000000..d3c3d8de2db --- /dev/null +++ b/packages/playground/php-bridge/src/lib/playground-php-bridge.spec.ts @@ -0,0 +1,7 @@ +import { playgroundPhpBridge } from './playground-php-bridge'; + +describe('playgroundPhpBridge', () => { + it('should work', () => { + expect(playgroundPhpBridge()).toEqual('playground-php-bridge'); + }); +}); diff --git a/packages/playground/php-bridge/src/lib/playground-php-bridge.ts b/packages/playground/php-bridge/src/lib/playground-php-bridge.ts new file mode 100644 index 00000000000..a5e7884c786 --- /dev/null +++ b/packages/playground/php-bridge/src/lib/playground-php-bridge.ts @@ -0,0 +1,3 @@ +export function playgroundPhpBridge(): string { + return 'playground-php-bridge'; +} diff --git a/packages/playground/blueprints/src/lib/utils/zip-functions.php b/packages/playground/php-bridge/src/playground-library.php similarity index 71% rename from packages/playground/blueprints/src/lib/utils/zip-functions.php rename to packages/playground/php-bridge/src/playground-library.php index a3fddee344a..908cc9d9e77 100644 --- a/packages/playground/blueprints/src/lib/utils/zip-functions.php +++ b/packages/playground/php-bridge/src/playground-library.php @@ -58,17 +58,40 @@ function join_paths() return preg_replace('#/+#', '/', join('/', $paths)); } -function unzip($zipPath, $extractTo, $overwrite = true) +function unzip($zipPath, $extractTo, $options = array()) { + $except = array_key_exists('except', $options) ? $options['except'] : array(); if (!is_dir($extractTo)) { mkdir($extractTo, 0777, true); } $zip = new ZipArchive; $res = $zip->open($zipPath); - if ($res === TRUE) { - $zip->extractTo($extractTo); - $zip->close(); + try { + if ($res !== TRUE) { + throw new Exception("Unable to open zip file: $zipPath"); + } + + for ($i = 0; $i < $zip->numFiles; $i++) { + $filename = $zip->getNameIndex($i); + foreach ($except as $e) { + if (preg_match($e, $filename) === 1) { + continue 2; + } + } + echo $filename . "\n"; + $fileInfo = pathinfo($filename); + if (!empty($fileInfo['dirname'])) { + $dir = join_paths($extractTo, $fileInfo['dirname']); + if (!is_dir($dir)) { + mkdir($dir, 0777, true); + } + } + $zip->extractTo($extractTo, $filename); + } + chmod($extractTo, 0777); + } finally { + $zip->close(); } } diff --git a/packages/playground/php-bridge/tsconfig.json b/packages/playground/php-bridge/tsconfig.json new file mode 100644 index 00000000000..421e57804d8 --- /dev/null +++ b/packages/playground/php-bridge/tsconfig.json @@ -0,0 +1,22 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/playground/php-bridge/tsconfig.lib.json b/packages/playground/php-bridge/tsconfig.lib.json new file mode 100644 index 00000000000..672b0253c97 --- /dev/null +++ b/packages/playground/php-bridge/tsconfig.lib.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../../dist/out-tsc", + "declaration": true, + "types": ["node", "vite/client"] + }, + "include": ["src/**/*.ts"], + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] +} diff --git a/packages/playground/php-bridge/tsconfig.spec.json b/packages/playground/php-bridge/tsconfig.spec.json new file mode 100644 index 00000000000..ee9bf732a8e --- /dev/null +++ b/packages/playground/php-bridge/tsconfig.spec.json @@ -0,0 +1,25 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "types": [ + "vitest/globals", + "vitest/importMeta", + "vite/client", + "node", + "vitest" + ] + }, + "include": [ + "vite.config.ts", + "src/**/*.test.ts", + "src/**/*.spec.ts", + "src/**/*.test.tsx", + "src/**/*.spec.tsx", + "src/**/*.test.js", + "src/**/*.spec.js", + "src/**/*.test.jsx", + "src/**/*.spec.jsx", + "src/**/*.d.ts" + ] +} diff --git a/packages/playground/php-bridge/vite.config.ts b/packages/playground/php-bridge/vite.config.ts new file mode 100644 index 00000000000..7d508e36fc3 --- /dev/null +++ b/packages/playground/php-bridge/vite.config.ts @@ -0,0 +1,48 @@ +/// +import { defineConfig } from 'vite'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import { viteTsConfigPaths } from '../../vite-ts-config-paths'; +import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'; + +export default defineConfig({ + cacheDir: '../../../node_modules/.vite/playground-php-bridge', + + plugins: [ + nxViteTsPaths(), + viteTsConfigPaths({ + root: '../../../', + }), + ], + + // Uncomment this if you are using workers. + // worker: { + // plugins: [ nxViteTsPaths() ], + // }, + + // Configuration for building your library. + // See: https://vitejs.dev/guide/build.html#library-mode + build: { + lib: { + // Could also be a dictionary or array of multiple entry points. + entry: 'src/index.ts', + name: 'playground-php-bridge', + fileName: 'index', + // Change this to the formats you want to support. + // Don't forget to update your package.json as well. + formats: ['es', 'cjs'], + }, + rollupOptions: { + // External packages that should not be bundled into your library. + external: [], + }, + }, + + test: { + globals: true, + cache: { + dir: '../../../node_modules/.vitest', + }, + environment: 'node', + include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'], + }, +}); diff --git a/packages/playground/blueprints/src/lib/playground-mu-plugin/0-playground.php b/packages/playground/remote/src/lib/worker-libs/playground-mu-plugin/0-playground.php similarity index 100% rename from packages/playground/blueprints/src/lib/playground-mu-plugin/0-playground.php rename to packages/playground/remote/src/lib/worker-libs/playground-mu-plugin/0-playground.php diff --git a/packages/playground/blueprints/src/lib/playground-mu-plugin/playground-includes/wp_http_dummy.php b/packages/playground/remote/src/lib/worker-libs/playground-mu-plugin/playground-includes/wp_http_dummy.php similarity index 100% rename from packages/playground/blueprints/src/lib/playground-mu-plugin/playground-includes/wp_http_dummy.php rename to packages/playground/remote/src/lib/worker-libs/playground-mu-plugin/playground-includes/wp_http_dummy.php diff --git a/packages/playground/blueprints/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php b/packages/playground/remote/src/lib/worker-libs/playground-mu-plugin/playground-includes/wp_http_fetch.php similarity index 100% rename from packages/playground/blueprints/src/lib/playground-mu-plugin/playground-includes/wp_http_fetch.php rename to packages/playground/remote/src/lib/worker-libs/playground-mu-plugin/playground-includes/wp_http_fetch.php diff --git a/packages/playground/blueprints/src/lib/setup-mu-plugins.ts b/packages/playground/remote/src/lib/worker-libs/setup-mu-plugins.ts similarity index 82% rename from packages/playground/blueprints/src/lib/setup-mu-plugins.ts rename to packages/playground/remote/src/lib/worker-libs/setup-mu-plugins.ts index 2b375e1f7e0..b3e06efc69d 100644 --- a/packages/playground/blueprints/src/lib/setup-mu-plugins.ts +++ b/packages/playground/remote/src/lib/worker-libs/setup-mu-plugins.ts @@ -7,9 +7,9 @@ import transportDummy from './playground-mu-plugin/playground-includes/wp_http_d /** @ts-ignore */ import playgroundMuPlugin from './playground-mu-plugin/0-playground.php?raw'; import { UniversalPHP, writeFiles } from '@php-wasm/universal'; -import { unzip } from './steps/unzip'; +import { unzip } from '@wp-playground/php-bridge'; -export async function installPlaygroundMuPlugin(php: UniversalPHP) { +export async function backfillPlaygroundMuPlugin(php: UniversalPHP) { const muPluginsPath = joinPaths( await php.documentRoot, 'wp-content/mu-plugins/' @@ -21,11 +21,8 @@ export async function installPlaygroundMuPlugin(php: UniversalPHP) { }); } -export async function installSqliteMuPlugin( - php: UniversalPHP, - snapshotPath: string -) { - await ensureSqliteMuPlugin(php, snapshotPath); +export async function backfillSqliteMuPlugin(php: UniversalPHP) { + await ensureSqliteMuPlugin(php); await activateSqliteMuPlugin(php); } @@ -38,26 +35,29 @@ export async function installSqliteMuPlugin( * The same logic is present in packages/playground/wordpress/build/Dockerfile * be sure to keep it in sync. */ -async function ensureSqliteMuPlugin(php: UniversalPHP, snapshotPath: string) { +async function ensureSqliteMuPlugin(php: UniversalPHP) { + const documentRoot = await php.documentRoot; + const sqliteMuPluginPath = 'wp-content/plugins/sqlite-database-integration'; - if (await php.fileExists(joinPaths(snapshotPath, sqliteMuPluginPath))) { + if (await php.fileExists(joinPaths(documentRoot, sqliteMuPluginPath))) { // The SQLite plugin is present in the imported snapshot, we're good. return false; } // Otherwise, let's download and install the SQLite plugin - const muPluginsPath = joinPaths(snapshotPath, 'wp-content/mu-plugins/'); + const muPluginsPath = joinPaths(documentRoot, 'wp-content/mu-plugins/'); const plugin = await fetch( 'https://downloads.wordpress.org/plugin/sqlite-database-integration.zip' ); - await unzip(php, { - // The zip file contains a directory with the same name as the plugin. - extractToPath: muPluginsPath, - zipFile: new File( + await unzip( + php, + new File( [await plugin.blob()], 'sqlite-database-integration.latest.zip' ), - }); + // The zip file contains a directory with the same name as the plugin. + muPluginsPath + ); return true; } diff --git a/packages/playground/remote/src/lib/worker-libs/setup-snapshot.ts b/packages/playground/remote/src/lib/worker-libs/setup-snapshot.ts new file mode 100644 index 00000000000..73ec6c26a73 --- /dev/null +++ b/packages/playground/remote/src/lib/worker-libs/setup-snapshot.ts @@ -0,0 +1,154 @@ +import { joinPaths } from '@php-wasm/util'; +import { UniversalPHP } from '@php-wasm/universal'; +import { + runWpInstallationWizard, + defineWpConfigConsts, +} from '@wp-playground/blueprints'; +import { unzip } from '@wp-playground/php-bridge'; + +export async function unzipSnapshot(php: UniversalPHP, snapshotZip: File) { + const targetPath = await php.documentRoot; + await unzip(php, snapshotZip, targetPath); + + // Ensure the mu-plugins directory is present in the extracted snapshot. + if (await php.fileExists(joinPaths(targetPath, 'wp-content'))) { + await php.mkdir(joinPaths(targetPath, 'wp-content', 'mu-plugins')); + } + + // The SQLite plugin used to be a regular plugin in old Playground exports. + // This won't work anymore. Let's be nice and clean it up if needed. + const sqlitePluginPath = joinPaths( + targetPath, + 'wp-content/plugins/sqlite-database-integration' + ); + await removePath(php, sqlitePluginPath); + + // If the snapshot contains a wp-content directory, we're done. + const files = await php.listFiles(targetPath); + const filesSet = new Set(files); + if (filesSet.has('wp-content')) { + return; + } + + // wp-content not found, let's find the WordPress core files in + // the extracted snapshot. + // @TODO do not infer the path from the snapshot contents. + // Instead, let the consumer of this API provide the path. + let pathInZip = ''; + if (filesSet.size === 1 && !filesSet.has('wp-content')) { + // If there's only one directory in the snapshot, use that as the import path. + pathInZip = files[0]; + } else if (filesSet.has('wordpress')) { + // If importing a WordPress.org zip, it may contain a top-level + // directory named "wordpress". If so, use that as the import path. + pathInZip = 'wordpress'; + } else if (filesSet.has('build')) { + // WordPress CI artifacts are in a "build" directory. + pathInZip = 'build'; + } + const importedFilesPath = joinPaths(targetPath, pathInZip); + await moveContents(php, importedFilesPath, targetPath); + await removePath(php, importedFilesPath); + + if (!(await php.fileExists(joinPaths(targetPath, 'wp-content')))) { + throw new Error( + 'The snapshot does not contain a wp-content directory.' + ); + } +} + +export async function backfillWordPressCore( + php: UniversalPHP, + wpCoreZip: File +) { + const documentRoot = await php.documentRoot; + const files = await php.listFiles(documentRoot); + if (files.includes('wp-includes') && files.includes('wp-admin')) { + // Core files are already present in the document root, nothing to do. + return; + } + + const filesSet = new Set(files); + const noop = '#^$#'; + await unzip(php, wpCoreZip, documentRoot, { + except: [ + // Do not overwrite wp-content files if it's already present. + filesSet.has('wp-content') ? '#^wp-content#' : noop, + // Do not overwrite wp-config.php if it's already present. + filesSet.has('wp-config.php') ? '#^wp-config.php$#' : noop, + ], + }); +} + +export async function backfillWpConfig(php: UniversalPHP) { + const targetPath = await php.documentRoot; + + // Ensure wp-config.php is present in the extracted snapshot. + if (!(await php.fileExists(joinPaths(targetPath, 'wp-config.php')))) { + const samplePath = joinPaths(targetPath, 'wp-config-sample.php'); + const wpConfig = (await php.fileExists(samplePath)) + ? await php.readFileAsText(samplePath) + : ` { + await php.mkdir(to); + const files = await php.listFiles(from); + for (const file of files) { + if (except.includes(file)) { + continue; + } + await php.mv(joinPaths(from, file), joinPaths(to, file)); + } +} + +export async function backfillDatabase(php: UniversalPHP) { + // Run the installation wizard if the database is missing + const dbExists = await php.fileExists( + joinPaths( + await php.documentRoot, + 'wp-content', + 'database', + '.ht.sqlite' + ) + ); + if (!dbExists) { + await runWpInstallationWizard(php, {}); + } +} + +async function removePath(playground: UniversalPHP, path: string) { + if (await playground.fileExists(path)) { + if (await playground.isDir(path)) { + await playground.rmdir(path); + } else { + await playground.unlink(path); + } + } +} diff --git a/packages/playground/remote/src/lib/worker-thread.ts b/packages/playground/remote/src/lib/worker-thread.ts index 6697f423d4b..758effe06fe 100644 --- a/packages/playground/remote/src/lib/worker-thread.ts +++ b/packages/playground/remote/src/lib/worker-thread.ts @@ -25,13 +25,19 @@ import { bindOpfs, playgroundAvailableInOpfs, } from './opfs/bind-opfs'; -import { - defineSiteUrl, - linkSnapshot, - setSnapshot, -} from '@wp-playground/blueprints'; import { joinPaths } from '@php-wasm/util'; +import { defineWpConfigConsts } from '@wp-playground/blueprints'; +import { + backfillDatabase, + backfillWordPressCore, + backfillWpConfig, + unzipSnapshot, +} from './worker-libs/setup-snapshot'; +import { + backfillPlaygroundMuPlugin, + backfillSqliteMuPlugin, +} from './worker-libs/setup-mu-plugins'; // post message to parent self.postMessage('worker-script-started'); @@ -92,19 +98,20 @@ const scopedSiteUrl = setURLScope(wordPressSiteUrl, scope).toString(); const monitor = new EmscriptenDownloadMonitor(); // Start downloading WordPress if needed -let wordPressRequest = null; +let snapshotRequest = null; +let wpCoreRequest = 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 = monitor.monitorFetch(fetch(requestedWPVersion)); - } else { - const wpDetails = getWordPressModuleDetails(wpVersion); - monitor.expectAssets({ - [wpDetails.url]: wpDetails.size, - }); - wordPressRequest = monitor.monitorFetch(fetch(wpDetails.url)); + snapshotRequest = monitor.monitorFetch(fetch(requestedWPVersion)); } + + const wpDetails = getWordPressModuleDetails(wpVersion); + monitor.expectAssets({ + [wpDetails.url]: wpDetails.size, + }); + wpCoreRequest = monitor.monitorFetch(fetch(wpDetails.url)); } const php = new WebPHP(undefined, { @@ -230,19 +237,29 @@ try { if (startupOptions.sapiName) { await php.setSapiName(startupOptions.sapiName); } - const docroot = php.documentRoot; + php.mkdir(php.documentRoot); // If WordPress isn't already installed, download and extract it from // the zip file. if (!wordPressAvailableInOPFS) { - const snapshot = new File( - [await (await wordPressRequest!).blob()], + if (snapshotRequest) { + const snapshotZip = new File( + [await (await snapshotRequest!).blob()], + 'snapshot.zip' + ); + await unzipSnapshot(php, snapshotZip); + } + const wpCoreZip = new File( + [await (await wpCoreRequest!).blob()], 'wp.zip' ); - await setSnapshot(php, snapshot); + await backfillWordPressCore(php, wpCoreZip); } - await linkSnapshot(php); + await backfillWpConfig(php); + await backfillSqliteMuPlugin(php); + await backfillPlaygroundMuPlugin(php); + await backfillDatabase(php); if (virtualOpfsDir) { await bindOpfs({ @@ -253,11 +270,17 @@ try { } // Create phpinfo.php + const docroot = php.documentRoot; php.writeFile(joinPaths(docroot, 'phpinfo.php'), '