diff --git a/.eslintignore b/.eslintignore index 34950a60faa..b25dd381547 100644 --- a/.eslintignore +++ b/.eslintignore @@ -8,6 +8,7 @@ packages/playground/wordpress-builds/src/wordpress packages/playground/wordpress-builds/public packages/playground/sync/src/test/wp-* packages/php-wasm/node/src/test/__test* +packages/playground/cli/src/edit-markdown/wp-markdown-editor *.timestamp-1678999213403.mjs .local .vscode diff --git a/.gitignore b/.gitignore index 0e47c5b83e4..bb7afd91e27 100644 --- a/.gitignore +++ b/.gitignore @@ -72,6 +72,10 @@ php.js.bak # we do not want to commit it to the repository. packages/php-wasm/cli/src/ca-bundle.crt +# Downloaded from adamziel/wp-extensions by playground-cli's build. +packages/playground/cli/src/edit-markdown/wp-markdown-editor/ +packages/playground/cli/src/edit-markdown/.tmp/ + # PHPUnit .phpunit.result.cache diff --git a/.nxignore b/.nxignore index 93ae590d3e4..da50fdf4a6b 100644 --- a/.nxignore +++ b/.nxignore @@ -9,6 +9,7 @@ __pycache__ packages/playground/wordpress-builds/src/wordpress packages/playground/wordpress-builds/public packages/php-wasm/node/src/test/__test* +packages/playground/cli/src/edit-markdown/vendor/php-toolkit *.timestamp-1678999213403.mjs .local .vscode diff --git a/CHANGELOG.md b/CHANGELOG.md index e725c7e29fa..4ccfbbd1405 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project are documented in this file by a CI job that runs on every NPM release. The file follows the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. -## [v3.1.33] (2026-05-14) +## [v3.1.33] (2026-05-14) ### PHP WebAssembly @@ -20,8 +20,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.32] (2026-05-14) +## [v3.1.32] (2026-05-14) ### PHP WebAssembly @@ -37,8 +36,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.31] (2026-05-14) +## [v3.1.31] (2026-05-14) ### PHP WebAssembly @@ -64,8 +62,7 @@ The following contributors merged PRs in this release: @adamziel @akirk @bgrgicak @fellyph - -## [v3.1.30] (2026-05-11) +## [v3.1.30] (2026-05-11) ### Bug Fixes @@ -81,8 +78,7 @@ The following contributors merged PRs in this release: @akirk @chubes4 - -## [v3.1.29] (2026-05-07) +## [v3.1.29] (2026-05-07) ### Blueprints @@ -90,7 +86,6 @@ The following contributors merged PRs in this release: ### Tools - #### PHP WebAssembly - [Package] Exclude unused dependencies when building `package.json` files. ([#3232](https://github.com/WordPress/wordpress-playground/pull/3232)) @@ -105,8 +100,7 @@ The following contributors merged PRs in this release: @adamziel @akirk @mho22 - -## [v3.1.28] (2026-05-05) +## [v3.1.28] (2026-05-05) ### PHP WebAssembly @@ -127,13 +121,9 @@ The following contributors merged PRs in this release: @adamziel @ashfame @JanJakes +## [v3.1.27] (2026-05-04) -## [v3.1.27] (2026-05-04) - - - - -## [v3.1.26] (2026-05-03) +## [v3.1.26] (2026-05-03) ### Documentation @@ -154,12 +144,10 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.25] (2026-05-03) +## [v3.1.25] (2026-05-03) ### Tools - #### Website - [Networking] Set curl.cainfo alongside openssl.cafile. ([#3583](https://github.com/WordPress/wordpress-playground/pull/3583)) @@ -184,17 +172,12 @@ The following contributors merged PRs in this release: @adamziel +## [v3.1.24] (2026-05-01) -## [v3.1.24] (2026-05-01) - - - - -## [v3.1.23] (2026-05-01) +## [v3.1.23] (2026-05-01) ### Public API - #### Blueprints - [Client] Restore bundled type declarations. ([#3576](https://github.com/WordPress/wordpress-playground/pull/3576)) @@ -205,8 +188,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.22] (2026-05-01) +## [v3.1.22] (2026-05-01) ### Blueprints @@ -304,8 +286,7 @@ The following contributors merged PRs in this release: @adamsilverstein @adamziel @apeatling @beryl-dlg @bgrgicak @fellyph @JanJakes @mho22 @pento @shail-mehta - -## [v3.1.21] (2026-04-20) +## [v3.1.21] (2026-04-20) ### PHP WebAssembly @@ -322,12 +303,10 @@ The following contributors merged PRs in this release: @adamziel @JanJakes - -## [v3.1.20] (2026-04-16) +## [v3.1.20] (2026-04-16) ### Tools - #### GitHub integration - [Github Actions] Fix GitHub release publishing wrong version. ([#3488](https://github.com/WordPress/wordpress-playground/pull/3488)) @@ -366,8 +345,7 @@ The following contributors merged PRs in this release: @adamziel @ashfame @fellyph @mho22 @perashanid - -## [v3.1.19] (2026-04-13) +## [v3.1.19] (2026-04-13) ### Documentation @@ -401,13 +379,9 @@ The following contributors merged PRs in this release: @ashfame @brandonpayton @dd32 @fellyph @JanJakes @mho22 @perashanid @Rima1889 +## [v3.1.18] (2026-04-07) -## [v3.1.18] (2026-04-07) - - - - -## [v3.1.17] (2026-04-07) +## [v3.1.17] (2026-04-07) ### Enhancements @@ -427,8 +401,7 @@ The following contributors merged PRs in this release: @adamziel @fellyph - -## [v3.1.16] (2026-04-06) +## [v3.1.16] (2026-04-06) ### Website @@ -452,8 +425,7 @@ The following contributors merged PRs in this release: @bgrgicak @JanJakes @mho22 @noruzzamans @shimotmk - -## [v3.1.15] (2026-03-31) +## [v3.1.15] (2026-03-31) ### PHP WebAssembly @@ -465,12 +437,11 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.14] (2026-03-30) +## [v3.1.14] (2026-03-30) ### PHP WebAssembly -- [Redis] va_arg long to va_arg zend_long for WASM32 ABI compatibility. ([#3417](https://github.com/WordPress/wordpress-playground/pull/3417)) +- [Redis] va_arg long to va_arg zend_long for WASM32 ABI compatibility. ([#3417](https://github.com/WordPress/wordpress-playground/pull/3417)) ### Internal @@ -499,8 +470,7 @@ The following contributors merged PRs in this release: @adamziel @beryl-dlg @bgrgicak @JanJakes @mho22 @perashanid @shimotmk @wojtekn - -## [v3.1.13] (2026-03-23) +## [v3.1.13] (2026-03-23) ### Enhancements @@ -518,7 +488,7 @@ The following contributors merged PRs in this release: - CLI: Add site editor performance benchmark. ([#3408](https://github.com/WordPress/wordpress-playground/pull/3408)) -### +### - CLI]: Consider it a lint error for CLI to depend on large Playground web packages. ([#3410](https://github.com/WordPress/wordpress-playground/pull/3410)) - Claude] Harden allow/deny lists and clarify dev server behavior. ([#3373](https://github.com/WordPress/wordpress-playground/pull/3373)) @@ -537,12 +507,10 @@ The following contributors merged PRs in this release: @adamziel @ashfame @bgrgicak @brandonpayton @wojtekn - -## [v3.1.12] (2026-03-16) +## [v3.1.12] (2026-03-16) ### Enhancements - #### Personal Playground - Remove Google Analytics from personal-wp. ([#3381](https://github.com/WordPress/wordpress-playground/pull/3381)) @@ -571,8 +539,7 @@ The following contributors merged PRs in this release: @adamziel @ashfame @brandonpayton @fellyph @zaerl - -## [v3.1.11] (2026-03-12) +## [v3.1.11] (2026-03-12) ### Enhancements @@ -580,7 +547,7 @@ The following contributors merged PRs in this release: ### Various -- [PHP] Mount parent directory for file symlinks so __DIR__ works. ([#3377](https://github.com/WordPress/wordpress-playground/pull/3377)) +- [PHP] Mount parent directory for file symlinks so **DIR** works. ([#3377](https://github.com/WordPress/wordpress-playground/pull/3377)) ### Contributors @@ -588,8 +555,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.10] (2026-03-12) +## [v3.1.10] (2026-03-12) ### Various @@ -601,8 +567,7 @@ The following contributors merged PRs in this release: @brandonpayton - -## [v3.1.9] (2026-03-11) +## [v3.1.9] (2026-03-11) ### Enhancements @@ -624,8 +589,7 @@ The following contributors merged PRs in this release: @adamziel @bgrgicak @brandonpayton @mho22 - -## [v3.1.8] (2026-03-10) +## [v3.1.8] (2026-03-10) ### Website @@ -641,13 +605,9 @@ The following contributors merged PRs in this release: @bgrgicak @mho22 +## [v3.1.7] (2026-03-10) -## [v3.1.7] (2026-03-10) - - - - -## [v3.1.6] (2026-03-10) +## [v3.1.6] (2026-03-10) ### PHP WebAssembly @@ -669,8 +629,7 @@ The following contributors merged PRs in this release: @adamziel @bgrgicak @fellyph @pkevan - -## [v3.1.5] (2026-03-09) +## [v3.1.5] (2026-03-09) ### Documentation @@ -703,8 +662,7 @@ The following contributors merged PRs in this release: @adamziel @andreilupu @bcotrim @brandonpayton @dd32 @fellyph @JanJakes @mho22 - -## [v3.1.3] (2026-03-02) +## [v3.1.3] (2026-03-02) ### Tools @@ -747,8 +705,7 @@ The following contributors merged PRs in this release: @adamziel @brandonpayton @dd32 @epeicher @fellyph @fredrikekelund @JanJakes @mho22 @n8finch @zaerl - -## [v3.1.2] (2026-02-23) +## [v3.1.2] (2026-02-23) ### Tools @@ -773,8 +730,7 @@ The following contributors merged PRs in this release: @ashfame @bgrgicak @brandonpayton @epeicher @JanJakes - -## [v3.1.1] (2026-02-18) +## [v3.1.1] (2026-02-18) ### Bug Fixes @@ -786,8 +742,7 @@ The following contributors merged PRs in this release: @brandonpayton - -## [v3.1.0] (2026-02-18) +## [v3.1.0] (2026-02-18) ### Bug Fixes @@ -799,12 +754,11 @@ The following contributors merged PRs in this release: @brandonpayton - -## [v3.0.54] (2026-02-18) +## [v3.0.54] (2026-02-18) ### Blueprints -- Define $_SERVER['HTTP_HOST'] in the enableMultisite step. ([#3214](https://github.com/WordPress/wordpress-playground/pull/3214)) +- Define $\_SERVER['HTTP_HOST'] in the enableMultisite step. ([#3214](https://github.com/WordPress/wordpress-playground/pull/3214)) ### Tools @@ -824,8 +778,7 @@ The following contributors merged PRs in this release: @adamziel @bcotrim @bookchiq @JanJakes @noruzzamans @shimotmk - -## [v3.0.53] (2026-02-16) +## [v3.0.53] (2026-02-16) ### Various @@ -838,13 +791,9 @@ The following contributors merged PRs in this release: @brandonpayton +## [v3.0.52] (2026-02-12) -## [v3.0.52] (2026-02-12) - - - - -## [v3.0.51] (2026-02-12) +## [v3.0.51] (2026-02-12) ### Bug Fixes @@ -856,8 +805,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.50] (2026-02-12) +## [v3.0.50] (2026-02-12) ### PHP WebAssembly @@ -869,8 +817,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.49] (2026-02-12) +## [v3.0.49] (2026-02-12) ### Various @@ -882,8 +829,7 @@ The following contributors merged PRs in this release: @mho22 - -## [v3.0.48] (2026-02-11) +## [v3.0.48] (2026-02-11) ### Enhancements @@ -929,8 +875,7 @@ The following contributors merged PRs in this release: @adamziel @ashfame @bgrgicak @bph @brandonpayton @fellyph @JanJakes @mho22 @noruzzamans @Omcodes23 @shimotmk - -## [v3.0.47] (2026-02-02) +## [v3.0.47] (2026-02-02) ### Blueprints @@ -995,8 +940,7 @@ The following contributors merged PRs in this release: @adamziel @akirk @beryl-dlg @epeicher @fellyph @JanJakes @mho22 @noruzzamans - -## [v3.0.46] (2026-01-26) +## [v3.0.46] (2026-01-26) ### PHP WebAssembly @@ -1018,12 +962,10 @@ The following contributors merged PRs in this release: @adamziel @akirk @noruzzamans - -## [v3.0.45] (2026-01-22) +## [v3.0.45] (2026-01-22) ### Tools - #### PHP WebAssembly - [PHP] Redis as a dynamic extension for Node.js. ([#3129](https://github.com/WordPress/wordpress-playground/pull/3129)) @@ -1060,8 +1002,7 @@ The following contributors merged PRs in this release: @adamziel @akirk @beryl-dlg @noruzzamans @shimotmk - -## [v3.0.44] (2026-01-20) +## [v3.0.44] (2026-01-20) ### Enhancements @@ -1085,8 +1026,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.43] (2026-01-19) +## [v3.0.43] (2026-01-19) ### Enhancements @@ -1112,8 +1052,7 @@ The following contributors merged PRs in this release: @adamziel @fellyph - -## [v3.0.42] (2026-01-15) +## [v3.0.42] (2026-01-15) ### Enhancements @@ -1129,8 +1068,7 @@ The following contributors merged PRs in this release: @adamziel @fellyph - -## [v3.0.41] (2026-01-14) +## [v3.0.41] (2026-01-14) ### Enhancements @@ -1154,8 +1092,7 @@ The following contributors merged PRs in this release: @adamziel @fellyph @mho22 - -## [v3.0.40] (2026-01-12) +## [v3.0.40] (2026-01-12) ### Documentation @@ -1174,12 +1111,11 @@ The following contributors merged PRs in this release: @adamziel @fellyph @noruzzamans - -## [v3.0.39] (2026-01-07) +## [v3.0.39] (2026-01-07) ### PHP WebAssembly -- [CLI] Fix __dirname not defined error in intl extension. ([#3094](https://github.com/WordPress/wordpress-playground/pull/3094)) +- [CLI] Fix \_\_dirname not defined error in intl extension. ([#3094](https://github.com/WordPress/wordpress-playground/pull/3094)) ### Various @@ -1191,13 +1127,9 @@ The following contributors merged PRs in this release: @brandonpayton @iamsohilvahora +## [v3.0.38] (2026-01-06) -## [v3.0.38] (2026-01-06) - - - - -## [v3.0.37] (2026-01-06) +## [v3.0.37] (2026-01-06) ### PHP WebAssembly @@ -1218,13 +1150,9 @@ The following contributors merged PRs in this release: @mho22 @noruzzamans +## [v3.0.36] (2026-01-05) -## [v3.0.36] (2026-01-05) - - - - -## [v3.0.35] (2025-12-29) +## [v3.0.35] (2025-12-29) ### PHP WebAssembly @@ -1245,8 +1173,7 @@ The following contributors merged PRs in this release: @adamziel @noruzzamans @shimotmk - -## [v3.0.34] (2025-12-24) +## [v3.0.34] (2025-12-24) ### PHP WebAssembly @@ -1268,8 +1195,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.33] (2025-12-22) +## [v3.0.33] (2025-12-22) ### Website @@ -1281,8 +1207,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.32] (2025-12-18) +## [v3.0.32] (2025-12-18) ### PHP WebAssembly @@ -1299,13 +1224,9 @@ The following contributors merged PRs in this release: @adamziel +## [v3.0.31] (2025-12-17) -## [v3.0.31] (2025-12-17) - - - - -## [v3.0.30] (2025-12-17) +## [v3.0.30] (2025-12-17) ### Documentation @@ -1325,8 +1246,7 @@ The following contributors merged PRs in this release: @mho22 - -## [v3.0.29] (2025-12-17) +## [v3.0.29] (2025-12-17) ### Tools @@ -1346,13 +1266,9 @@ The following contributors merged PRs in this release: @adamziel @akirk +## [v3.0.28] (2025-12-17) -## [v3.0.28] (2025-12-17) - - - - -## [v3.0.27] (2025-12-16) +## [v3.0.27] (2025-12-16) ### Internal @@ -1364,13 +1280,9 @@ The following contributors merged PRs in this release: @adamziel +## [v3.0.26] (2025-12-16) -## [v3.0.26] (2025-12-16) - - - - -## [v3.0.25] (2025-12-16) +## [v3.0.25] (2025-12-16) ### Website @@ -1391,8 +1303,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.24] (2025-12-16) +## [v3.0.24] (2025-12-16) ### Website @@ -1408,8 +1319,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.23] (2025-12-16) +## [v3.0.23] (2025-12-16) ### Enhancements @@ -1473,7 +1383,6 @@ The following contributors merged PRs in this release: ### Experiments - #### GitHub integration - [ playground-storage ] Add `vite-plugin-dts` to Playground Storage. ([#3035](https://github.com/WordPress/wordpress-playground/pull/3035)) @@ -1544,7 +1453,7 @@ The following contributors merged PRs in this release: - [Docs] Fix API reference. ([#2905](https://github.com/WordPress/wordpress-playground/pull/2905)) - [Docs] Fix documentation site build failures. ([#2913](https://github.com/WordPress/wordpress-playground/pull/2913)) -### +### - Xdebug ] Relocate `xdebug` into shared library directory. ([#3045](https://github.com/WordPress/wordpress-playground/pull/3045)) @@ -1570,12 +1479,10 @@ The following contributors merged PRs in this release: @adamziel @akirk @andr3ribeiro @bgrgicak @brandonpayton @epeicher @fellyph @JanJakes @jeffpaul @mho22 @shimotmk @SirLouen @Utsav-Ladani @wojtekn - -## [v3.0.22] (2025-11-17) +## [v3.0.22] (2025-11-17) ### Enhancements - #### Boot Flow - [Boot] Verify permalink structure is actually set. ([#2902](https://github.com/WordPress/wordpress-playground/pull/2902)) @@ -1605,7 +1512,7 @@ The following contributors merged PRs in this release: - [CLI] Fix null and "latest" WP version resolution and improve unzip error message. ([#2889](https://github.com/WordPress/wordpress-playground/pull/2889)) - [CLI] Fix run-cli leak which was revealed by repeated runCLI() calls during test. ([#2888](https://github.com/WordPress/wordpress-playground/pull/2888)) -### +### - CLI] Allow API consumers to rely upon option validation and default values. ([#2883](https://github.com/WordPress/wordpress-playground/pull/2883)) @@ -1626,7 +1533,6 @@ The following contributors merged PRs in this release: @adamziel @brandonpayton @fellyph @mehrazmorshed @praful2111 @shimotmk @SirLouen @Successfulsebunya - ## [v3.0.21] (2025-11-10) ### Bug Fixes diff --git a/packages/docs/site/docs/main/changelog.md b/packages/docs/site/docs/main/changelog.md index 1d7997bbdd2..10e69ac1a89 100644 --- a/packages/docs/site/docs/main/changelog.md +++ b/packages/docs/site/docs/main/changelog.md @@ -3,7 +3,7 @@ title: Changelog slug: /changelog hide_table_of_contents: true mdx: - format: md + format: md --- # Changelog @@ -12,7 +12,7 @@ All notable changes to this project are documented in this file by a CI job that runs on every NPM release. The file follows the [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format. -## [v3.1.33] (2026-05-14) +## [v3.1.33] (2026-05-14) ### PHP WebAssembly @@ -28,8 +28,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.32] (2026-05-14) +## [v3.1.32] (2026-05-14) ### PHP WebAssembly @@ -45,8 +44,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.31] (2026-05-14) +## [v3.1.31] (2026-05-14) ### PHP WebAssembly @@ -72,8 +70,7 @@ The following contributors merged PRs in this release: @adamziel @akirk @bgrgicak @fellyph - -## [v3.1.30] (2026-05-11) +## [v3.1.30] (2026-05-11) ### Bug Fixes @@ -89,8 +86,7 @@ The following contributors merged PRs in this release: @akirk @chubes4 - -## [v3.1.29] (2026-05-07) +## [v3.1.29] (2026-05-07) ### Blueprints @@ -98,7 +94,6 @@ The following contributors merged PRs in this release: ### Tools - #### PHP WebAssembly - [Package] Exclude unused dependencies when building `package.json` files. ([#3232](https://github.com/WordPress/wordpress-playground/pull/3232)) @@ -113,8 +108,7 @@ The following contributors merged PRs in this release: @adamziel @akirk @mho22 - -## [v3.1.28] (2026-05-05) +## [v3.1.28] (2026-05-05) ### PHP WebAssembly @@ -135,13 +129,9 @@ The following contributors merged PRs in this release: @adamziel @ashfame @JanJakes +## [v3.1.27] (2026-05-04) -## [v3.1.27] (2026-05-04) - - - - -## [v3.1.26] (2026-05-03) +## [v3.1.26] (2026-05-03) ### Documentation @@ -162,12 +152,10 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.25] (2026-05-03) +## [v3.1.25] (2026-05-03) ### Tools - #### Website - [Networking] Set curl.cainfo alongside openssl.cafile. ([#3583](https://github.com/WordPress/wordpress-playground/pull/3583)) @@ -192,17 +180,12 @@ The following contributors merged PRs in this release: @adamziel +## [v3.1.24] (2026-05-01) -## [v3.1.24] (2026-05-01) - - - - -## [v3.1.23] (2026-05-01) +## [v3.1.23] (2026-05-01) ### Public API - #### Blueprints - [Client] Restore bundled type declarations. ([#3576](https://github.com/WordPress/wordpress-playground/pull/3576)) @@ -213,8 +196,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.22] (2026-05-01) +## [v3.1.22] (2026-05-01) ### Blueprints @@ -312,8 +294,7 @@ The following contributors merged PRs in this release: @adamsilverstein @adamziel @apeatling @beryl-dlg @bgrgicak @fellyph @JanJakes @mho22 @pento @shail-mehta - -## [v3.1.21] (2026-04-20) +## [v3.1.21] (2026-04-20) ### PHP WebAssembly @@ -330,12 +311,10 @@ The following contributors merged PRs in this release: @adamziel @JanJakes - -## [v3.1.20] (2026-04-16) +## [v3.1.20] (2026-04-16) ### Tools - #### GitHub integration - [Github Actions] Fix GitHub release publishing wrong version. ([#3488](https://github.com/WordPress/wordpress-playground/pull/3488)) @@ -374,8 +353,7 @@ The following contributors merged PRs in this release: @adamziel @ashfame @fellyph @mho22 @perashanid - -## [v3.1.19] (2026-04-13) +## [v3.1.19] (2026-04-13) ### Documentation @@ -409,13 +387,9 @@ The following contributors merged PRs in this release: @ashfame @brandonpayton @dd32 @fellyph @JanJakes @mho22 @perashanid @Rima1889 +## [v3.1.18] (2026-04-07) -## [v3.1.18] (2026-04-07) - - - - -## [v3.1.17] (2026-04-07) +## [v3.1.17] (2026-04-07) ### Enhancements @@ -435,8 +409,7 @@ The following contributors merged PRs in this release: @adamziel @fellyph - -## [v3.1.16] (2026-04-06) +## [v3.1.16] (2026-04-06) ### Website @@ -460,8 +433,7 @@ The following contributors merged PRs in this release: @bgrgicak @JanJakes @mho22 @noruzzamans @shimotmk - -## [v3.1.15] (2026-03-31) +## [v3.1.15] (2026-03-31) ### PHP WebAssembly @@ -473,12 +445,11 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.14] (2026-03-30) +## [v3.1.14] (2026-03-30) ### PHP WebAssembly -- [Redis] va_arg long to va_arg zend_long for WASM32 ABI compatibility. ([#3417](https://github.com/WordPress/wordpress-playground/pull/3417)) +- [Redis] va_arg long to va_arg zend_long for WASM32 ABI compatibility. ([#3417](https://github.com/WordPress/wordpress-playground/pull/3417)) ### Internal @@ -507,8 +478,7 @@ The following contributors merged PRs in this release: @adamziel @beryl-dlg @bgrgicak @JanJakes @mho22 @perashanid @shimotmk @wojtekn - -## [v3.1.13] (2026-03-23) +## [v3.1.13] (2026-03-23) ### Enhancements @@ -526,7 +496,7 @@ The following contributors merged PRs in this release: - CLI: Add site editor performance benchmark. ([#3408](https://github.com/WordPress/wordpress-playground/pull/3408)) -### +### - CLI]: Consider it a lint error for CLI to depend on large Playground web packages. ([#3410](https://github.com/WordPress/wordpress-playground/pull/3410)) - Claude] Harden allow/deny lists and clarify dev server behavior. ([#3373](https://github.com/WordPress/wordpress-playground/pull/3373)) @@ -545,12 +515,10 @@ The following contributors merged PRs in this release: @adamziel @ashfame @bgrgicak @brandonpayton @wojtekn - -## [v3.1.12] (2026-03-16) +## [v3.1.12] (2026-03-16) ### Enhancements - #### Personal Playground - Remove Google Analytics from personal-wp. ([#3381](https://github.com/WordPress/wordpress-playground/pull/3381)) @@ -579,8 +547,7 @@ The following contributors merged PRs in this release: @adamziel @ashfame @brandonpayton @fellyph @zaerl - -## [v3.1.11] (2026-03-12) +## [v3.1.11] (2026-03-12) ### Enhancements @@ -588,7 +555,7 @@ The following contributors merged PRs in this release: ### Various -- [PHP] Mount parent directory for file symlinks so __DIR__ works. ([#3377](https://github.com/WordPress/wordpress-playground/pull/3377)) +- [PHP] Mount parent directory for file symlinks so **DIR** works. ([#3377](https://github.com/WordPress/wordpress-playground/pull/3377)) ### Contributors @@ -596,8 +563,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.1.10] (2026-03-12) +## [v3.1.10] (2026-03-12) ### Various @@ -609,8 +575,7 @@ The following contributors merged PRs in this release: @brandonpayton - -## [v3.1.9] (2026-03-11) +## [v3.1.9] (2026-03-11) ### Enhancements @@ -632,8 +597,7 @@ The following contributors merged PRs in this release: @adamziel @bgrgicak @brandonpayton @mho22 - -## [v3.1.8] (2026-03-10) +## [v3.1.8] (2026-03-10) ### Website @@ -649,13 +613,9 @@ The following contributors merged PRs in this release: @bgrgicak @mho22 +## [v3.1.7] (2026-03-10) -## [v3.1.7] (2026-03-10) - - - - -## [v3.1.6] (2026-03-10) +## [v3.1.6] (2026-03-10) ### PHP WebAssembly @@ -677,8 +637,7 @@ The following contributors merged PRs in this release: @adamziel @bgrgicak @fellyph @pkevan - -## [v3.1.5] (2026-03-09) +## [v3.1.5] (2026-03-09) ### Documentation @@ -711,8 +670,7 @@ The following contributors merged PRs in this release: @adamziel @andreilupu @bcotrim @brandonpayton @dd32 @fellyph @JanJakes @mho22 - -## [v3.1.3] (2026-03-02) +## [v3.1.3] (2026-03-02) ### Tools @@ -755,8 +713,7 @@ The following contributors merged PRs in this release: @adamziel @brandonpayton @dd32 @epeicher @fellyph @fredrikekelund @JanJakes @mho22 @n8finch @zaerl - -## [v3.1.2] (2026-02-23) +## [v3.1.2] (2026-02-23) ### Tools @@ -781,8 +738,7 @@ The following contributors merged PRs in this release: @ashfame @bgrgicak @brandonpayton @epeicher @JanJakes - -## [v3.1.1] (2026-02-18) +## [v3.1.1] (2026-02-18) ### Bug Fixes @@ -794,8 +750,7 @@ The following contributors merged PRs in this release: @brandonpayton - -## [v3.1.0] (2026-02-18) +## [v3.1.0] (2026-02-18) ### Bug Fixes @@ -807,12 +762,11 @@ The following contributors merged PRs in this release: @brandonpayton - -## [v3.0.54] (2026-02-18) +## [v3.0.54] (2026-02-18) ### Blueprints -- Define $_SERVER['HTTP_HOST'] in the enableMultisite step. ([#3214](https://github.com/WordPress/wordpress-playground/pull/3214)) +- Define $\_SERVER['HTTP_HOST'] in the enableMultisite step. ([#3214](https://github.com/WordPress/wordpress-playground/pull/3214)) ### Tools @@ -832,8 +786,7 @@ The following contributors merged PRs in this release: @adamziel @bcotrim @bookchiq @JanJakes @noruzzamans @shimotmk - -## [v3.0.53] (2026-02-16) +## [v3.0.53] (2026-02-16) ### Various @@ -846,13 +799,9 @@ The following contributors merged PRs in this release: @brandonpayton +## [v3.0.52] (2026-02-12) -## [v3.0.52] (2026-02-12) - - - - -## [v3.0.51] (2026-02-12) +## [v3.0.51] (2026-02-12) ### Bug Fixes @@ -864,8 +813,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.50] (2026-02-12) +## [v3.0.50] (2026-02-12) ### PHP WebAssembly @@ -877,8 +825,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.49] (2026-02-12) +## [v3.0.49] (2026-02-12) ### Various @@ -890,8 +837,7 @@ The following contributors merged PRs in this release: @mho22 - -## [v3.0.48] (2026-02-11) +## [v3.0.48] (2026-02-11) ### Enhancements @@ -937,8 +883,7 @@ The following contributors merged PRs in this release: @adamziel @ashfame @bgrgicak @bph @brandonpayton @fellyph @JanJakes @mho22 @noruzzamans @Omcodes23 @shimotmk - -## [v3.0.47] (2026-02-02) +## [v3.0.47] (2026-02-02) ### Blueprints @@ -1003,8 +948,7 @@ The following contributors merged PRs in this release: @adamziel @akirk @beryl-dlg @epeicher @fellyph @JanJakes @mho22 @noruzzamans - -## [v3.0.46] (2026-01-26) +## [v3.0.46] (2026-01-26) ### PHP WebAssembly @@ -1026,12 +970,10 @@ The following contributors merged PRs in this release: @adamziel @akirk @noruzzamans - -## [v3.0.45] (2026-01-22) +## [v3.0.45] (2026-01-22) ### Tools - #### PHP WebAssembly - [PHP] Redis as a dynamic extension for Node.js. ([#3129](https://github.com/WordPress/wordpress-playground/pull/3129)) @@ -1068,8 +1010,7 @@ The following contributors merged PRs in this release: @adamziel @akirk @beryl-dlg @noruzzamans @shimotmk - -## [v3.0.44] (2026-01-20) +## [v3.0.44] (2026-01-20) ### Enhancements @@ -1093,8 +1034,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.43] (2026-01-19) +## [v3.0.43] (2026-01-19) ### Enhancements @@ -1120,8 +1060,7 @@ The following contributors merged PRs in this release: @adamziel @fellyph - -## [v3.0.42] (2026-01-15) +## [v3.0.42] (2026-01-15) ### Enhancements @@ -1137,8 +1076,7 @@ The following contributors merged PRs in this release: @adamziel @fellyph - -## [v3.0.41] (2026-01-14) +## [v3.0.41] (2026-01-14) ### Enhancements @@ -1162,8 +1100,7 @@ The following contributors merged PRs in this release: @adamziel @fellyph @mho22 - -## [v3.0.40] (2026-01-12) +## [v3.0.40] (2026-01-12) ### Documentation @@ -1182,12 +1119,11 @@ The following contributors merged PRs in this release: @adamziel @fellyph @noruzzamans - -## [v3.0.39] (2026-01-07) +## [v3.0.39] (2026-01-07) ### PHP WebAssembly -- [CLI] Fix __dirname not defined error in intl extension. ([#3094](https://github.com/WordPress/wordpress-playground/pull/3094)) +- [CLI] Fix \_\_dirname not defined error in intl extension. ([#3094](https://github.com/WordPress/wordpress-playground/pull/3094)) ### Various @@ -1199,13 +1135,9 @@ The following contributors merged PRs in this release: @brandonpayton @iamsohilvahora +## [v3.0.38] (2026-01-06) -## [v3.0.38] (2026-01-06) - - - - -## [v3.0.37] (2026-01-06) +## [v3.0.37] (2026-01-06) ### PHP WebAssembly @@ -1226,13 +1158,9 @@ The following contributors merged PRs in this release: @mho22 @noruzzamans +## [v3.0.36] (2026-01-05) -## [v3.0.36] (2026-01-05) - - - - -## [v3.0.35] (2025-12-29) +## [v3.0.35] (2025-12-29) ### PHP WebAssembly @@ -1253,8 +1181,7 @@ The following contributors merged PRs in this release: @adamziel @noruzzamans @shimotmk - -## [v3.0.34] (2025-12-24) +## [v3.0.34] (2025-12-24) ### PHP WebAssembly @@ -1276,8 +1203,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.33] (2025-12-22) +## [v3.0.33] (2025-12-22) ### Website @@ -1289,8 +1215,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.32] (2025-12-18) +## [v3.0.32] (2025-12-18) ### PHP WebAssembly @@ -1307,13 +1232,9 @@ The following contributors merged PRs in this release: @adamziel +## [v3.0.31] (2025-12-17) -## [v3.0.31] (2025-12-17) - - - - -## [v3.0.30] (2025-12-17) +## [v3.0.30] (2025-12-17) ### Documentation @@ -1333,8 +1254,7 @@ The following contributors merged PRs in this release: @mho22 - -## [v3.0.29] (2025-12-17) +## [v3.0.29] (2025-12-17) ### Tools @@ -1354,13 +1274,9 @@ The following contributors merged PRs in this release: @adamziel @akirk +## [v3.0.28] (2025-12-17) -## [v3.0.28] (2025-12-17) - - - - -## [v3.0.27] (2025-12-16) +## [v3.0.27] (2025-12-16) ### Internal @@ -1372,13 +1288,9 @@ The following contributors merged PRs in this release: @adamziel +## [v3.0.26] (2025-12-16) -## [v3.0.26] (2025-12-16) - - - - -## [v3.0.25] (2025-12-16) +## [v3.0.25] (2025-12-16) ### Website @@ -1399,8 +1311,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.24] (2025-12-16) +## [v3.0.24] (2025-12-16) ### Website @@ -1416,8 +1327,7 @@ The following contributors merged PRs in this release: @adamziel - -## [v3.0.23] (2025-12-16) +## [v3.0.23] (2025-12-16) ### Enhancements @@ -1481,7 +1391,6 @@ The following contributors merged PRs in this release: ### Experiments - #### GitHub integration - [ playground-storage ] Add `vite-plugin-dts` to Playground Storage. ([#3035](https://github.com/WordPress/wordpress-playground/pull/3035)) @@ -1552,7 +1461,7 @@ The following contributors merged PRs in this release: - [Docs] Fix API reference. ([#2905](https://github.com/WordPress/wordpress-playground/pull/2905)) - [Docs] Fix documentation site build failures. ([#2913](https://github.com/WordPress/wordpress-playground/pull/2913)) -### +### - Xdebug ] Relocate `xdebug` into shared library directory. ([#3045](https://github.com/WordPress/wordpress-playground/pull/3045)) @@ -1578,12 +1487,10 @@ The following contributors merged PRs in this release: @adamziel @akirk @andr3ribeiro @bgrgicak @brandonpayton @epeicher @fellyph @JanJakes @jeffpaul @mho22 @shimotmk @SirLouen @Utsav-Ladani @wojtekn - -## [v3.0.22] (2025-11-17) +## [v3.0.22] (2025-11-17) ### Enhancements - #### Boot Flow - [Boot] Verify permalink structure is actually set. ([#2902](https://github.com/WordPress/wordpress-playground/pull/2902)) @@ -1613,7 +1520,7 @@ The following contributors merged PRs in this release: - [CLI] Fix null and "latest" WP version resolution and improve unzip error message. ([#2889](https://github.com/WordPress/wordpress-playground/pull/2889)) - [CLI] Fix run-cli leak which was revealed by repeated runCLI() calls during test. ([#2888](https://github.com/WordPress/wordpress-playground/pull/2888)) -### +### - CLI] Allow API consumers to rely upon option validation and default values. ([#2883](https://github.com/WordPress/wordpress-playground/pull/2883)) @@ -1634,7 +1541,6 @@ The following contributors merged PRs in this release: @adamziel @brandonpayton @fellyph @mehrazmorshed @praful2111 @shimotmk @SirLouen @Successfulsebunya - ## [v3.0.21] (2025-11-10) ### Bug Fixes diff --git a/packages/docs/site/docusaurus.config.js b/packages/docs/site/docusaurus.config.js index fbcfd2d6c0e..21effba20f3 100644 --- a/packages/docs/site/docusaurus.config.js +++ b/packages/docs/site/docusaurus.config.js @@ -25,6 +25,7 @@ const config = { projectName: 'wordpress-playground', // Usually your repo name. onBrokenLinks: 'throw', + onBrokenAnchors: 'ignore', markdown: { hooks: { diff --git a/packages/docs/site/src/components/BlueprintsAPI/BlueprintStep.tsx b/packages/docs/site/src/components/BlueprintsAPI/BlueprintStep.tsx index 48b53a3b8d1..bce4df14aa6 100644 --- a/packages/docs/site/src/components/BlueprintsAPI/BlueprintStep.tsx +++ b/packages/docs/site/src/components/BlueprintsAPI/BlueprintStep.tsx @@ -19,14 +19,17 @@ export default function BlueprintStep({ name }) { > ​ - - ​ - + {stepApi.stepId && stepApi.stepId !== name ? ( + + ​ + + ) : null} diff --git a/packages/php-wasm/compile-extension/README.md b/packages/php-wasm/compile-extension/README.md index ba00e2af427..fcd2861451e 100644 --- a/packages/php-wasm/compile-extension/README.md +++ b/packages/php-wasm/compile-extension/README.md @@ -190,6 +190,102 @@ const php = new PHP( Use `loadWithIniDirective: 'zend_extension'` for Zend extensions such as Xdebug. Use `extraFiles` and `env` for sidecar files needed by the extension. +## Building WordPress plugins backed by WASM + +For WordPress plugins, keep the native code and WordPress integration in two +layers: + +- Compile the native layer as a PHP.wasm extension. It should register PHP + functions or classes from C, C++, Rust, or another Emscripten-compatible + source. +- Load a small PHP bootstrap as an mu-plugin. The bootstrap calls WordPress + APIs such as `add_action()` and `add_filter()` and points those hooks at the + PHP functions/classes exposed by the WASM extension. + +Start with this directory layout: + +```text +hello-wasm/ +|-- bootstrap.php +|-- extension/ +| |-- config.m4 +| `-- hello_wasm.c +`-- wasm-wordpress-plugin.json +``` + +The native extension exposes regular PHP callables: + +```c +#include "php.h" + +PHP_FUNCTION(hello_wasm_render_text) +{ + RETURN_STRING("Hello from WASM"); +} +``` + +The bootstrap file contains the WordPress-facing code: + +```php +

%s

', + esc_html( hello_wasm_render_text() ) + ); +} +``` + +Build the extension: + +```bash +npx @php-wasm/compile-extension \ + --source ./extension \ + --name hello_wasm \ + --php-versions 8.4 \ + --out ./dist +``` + +Then create a Playground CLI descriptor that wires both layers: + +```json +{ + "slug": "hello-wasm", + "name": "Hello WASM", + "extension": { + "name": "hello_wasm", + "source": { + "format": "manifest", + "manifestUrl": "./dist/hello-wasm/manifest.json" + } + }, + "hooks": [ + { + "type": "filter", + "hook": "the_content", + "callback": "hello_wasm_render_content" + } + ] +} +``` + +Load it with: + +```bash +npx @wp-playground/cli@latest server \ + --php=8.4 \ + --wasm-wordpress-plugin=./hello-wasm.json +``` + +Use a `bootstrap` file in the descriptor when the plugin needs PHP wrappers, +capability checks, or object-oriented hook callbacks. Local manifest and +bootstrap paths resolve relative to the descriptor file. + +See `examples/hello-dolly-wasm` for a complete Hello Dolly-style plugin. It +builds a `hello_dolly_wasm` PHP.wasm extension, installs `bootstrap.php` as an +mu-plugin, and registers `admin_notices` / `admin_head` hooks through the +descriptor. + ## Dependencies The helper can only link WebAssembly objects built with the same Emscripten diff --git a/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/README.md b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/README.md new file mode 100644 index 00000000000..70457cac752 --- /dev/null +++ b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/README.md @@ -0,0 +1,50 @@ +# Hello Dolly as a WASM WordPress plugin + +This example mirrors the classic Hello Dolly plugin shape, but moves the +greeting provider into a PHP.wasm extension. WordPress still loads a normal PHP +bootstrap as an mu-plugin; that bootstrap registers WordPress hooks and calls a +PHP function exposed by the WASM extension. + +## Files + +- `extension/hello_dolly_wasm.c` defines the native PHP function + `hello_dolly_wasm_get_lyric()`. +- `extension/config.m4` is the standard `phpize` extension build file. +- `bootstrap.php` contains the WordPress integration code. +- `wasm-wordpress-plugin.json` tells Playground how to load the extension and + which WordPress hooks to register. + +## Build + +Docker is required because `@php-wasm/compile-extension` builds inside the same +Emscripten/PHP toolchain used by PHP.wasm. + +From this directory: + +```bash +npx @php-wasm/compile-extension \ + --source ./extension \ + --name hello_dolly_wasm \ + --php-versions 8.4 \ + --out ./dist +``` + +The command writes `./dist/manifest.json` and one `.so` artifact per requested +PHP version. + +## Run in Playground CLI + +```bash +npx @wp-playground/cli@latest server \ + --php=8.4 \ + --wasm-wordpress-plugin=./wasm-wordpress-plugin.json +``` + +The descriptor loads `./dist/manifest.json`, installs `bootstrap.php` as an +mu-plugin, and registers: + +- `admin_notices` -> `hello_dolly_wasm_render` +- `admin_head` -> `hello_dolly_wasm_css` + +Open `/wp-admin/` to see a random Hello Dolly-style greeting rendered by +WordPress, with the greeting text supplied by the WASM extension. diff --git a/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/bootstrap.php b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/bootstrap.php new file mode 100644 index 00000000000..45062917d4a --- /dev/null +++ b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/bootstrap.php @@ -0,0 +1,42 @@ +%s %s

', + esc_html__( 'Greeting from Hello Dolly WASM:', 'default' ), + esc_html( $lyric ) + ); +} + +function hello_dolly_wasm_css() { + echo " + + "; +} diff --git a/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/extension/config.m4 b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/extension/config.m4 new file mode 100644 index 00000000000..beaa0e73ad0 --- /dev/null +++ b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/extension/config.m4 @@ -0,0 +1,7 @@ +PHP_ARG_ENABLE([hello_dolly_wasm], [whether to enable hello_dolly_wasm], + [AS_HELP_STRING([--enable-hello_dolly_wasm], [Enable hello_dolly_wasm])], + [no]) + +if test "$PHP_HELLO_DOLLY_WASM" != "no"; then + PHP_NEW_EXTENSION([hello_dolly_wasm], [hello_dolly_wasm.c], [$ext_shared]) +fi diff --git a/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/extension/hello_dolly_wasm.c b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/extension/hello_dolly_wasm.c new file mode 100644 index 00000000000..417940e312c --- /dev/null +++ b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/extension/hello_dolly_wasm.c @@ -0,0 +1,64 @@ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "php.h" + +static const char *hello_dolly_wasm_lyrics[] = { + "Hello from a PHP.wasm extension", + "Native code can join WordPress hooks", + "Small modules can power focused features", + "The lyric picker is running in WebAssembly", + "WordPress rendered this through an mu-plugin", + "Hooks stay in PHP while logic moves to WASM", + "Playground loaded this module before PHP started", + "Compiled extensions can expose ordinary PHP functions", + "This greeting crossed the WASM boundary", + "Hello Dolly, Playground edition" +}; + +PHP_FUNCTION(hello_dolly_wasm_get_lyric) +{ + zend_long index = 0; + size_t lyric_count = sizeof(hello_dolly_wasm_lyrics) / sizeof(hello_dolly_wasm_lyrics[0]); + + ZEND_PARSE_PARAMETERS_START(0, 1) + Z_PARAM_OPTIONAL + Z_PARAM_LONG(index) + ZEND_PARSE_PARAMETERS_END(); + + if (index < 0) { + index = -index; + } + + RETURN_STRING(hello_dolly_wasm_lyrics[index % lyric_count]); +} + +ZEND_BEGIN_ARG_WITH_RETURN_TYPE_INFO_EX(arginfo_hello_dolly_wasm_get_lyric, 0, 0, IS_STRING, 0) + ZEND_ARG_TYPE_INFO(0, index, IS_LONG, 0) +ZEND_END_ARG_INFO() + +static const zend_function_entry hello_dolly_wasm_functions[] = { + PHP_FE(hello_dolly_wasm_get_lyric, arginfo_hello_dolly_wasm_get_lyric) + PHP_FE_END +}; + +zend_module_entry hello_dolly_wasm_module_entry = { + STANDARD_MODULE_HEADER, + "hello_dolly_wasm", + hello_dolly_wasm_functions, + NULL, + NULL, + NULL, + NULL, + NULL, + "0.1.0", + STANDARD_MODULE_PROPERTIES +}; + +#ifdef COMPILE_DL_HELLO_DOLLY_WASM +#ifdef ZTS +ZEND_TSRMLS_CACHE_DEFINE() +#endif +ZEND_GET_MODULE(hello_dolly_wasm) +#endif diff --git a/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/wasm-wordpress-plugin.json b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/wasm-wordpress-plugin.json new file mode 100644 index 00000000000..a934cd9c060 --- /dev/null +++ b/packages/php-wasm/compile-extension/examples/hello-dolly-wasm/wasm-wordpress-plugin.json @@ -0,0 +1,26 @@ +{ + "slug": "hello-dolly-wasm", + "name": "Hello Dolly WASM", + "description": "Displays a random Hello Dolly-style greeting in wp-admin, with the greeting provider implemented as a PHP.wasm extension.", + "version": "0.1.0", + "extension": { + "name": "hello_dolly_wasm", + "source": { + "format": "manifest", + "manifestUrl": "./dist/manifest.json" + } + }, + "bootstrap": "./bootstrap.php", + "hooks": [ + { + "type": "action", + "hook": "admin_notices", + "callback": "hello_dolly_wasm_render" + }, + { + "type": "action", + "hook": "admin_head", + "callback": "hello_dolly_wasm_css" + } + ] +} diff --git a/packages/playground/cli/README.md b/packages/playground/cli/README.md index 406dc2b5472..91544734a70 100644 --- a/packages/playground/cli/README.md +++ b/packages/playground/cli/README.md @@ -101,6 +101,8 @@ The `server` command supports the following optional arguments: - `--phpmyadmin[=]`: Install phpMyAdmin for database management. The phpMyAdmin URL will be printed after boot. Optionally specify a custom URL path (default: `/phpmyadmin`). - `--internal-cookie-store`: Enables Playground's internal cookie handling. When active, Playground uses an HttpCookieStore to manage and persist cookies across requests. If disabled, cookies are handled externally, like by a browser in Node.js. - `--php-extension=`: Load a custom PHP.wasm extension manifest before PHP starts. Accepts local paths, `file:` URLs, and `http(s):` URLs. Can be used multiple times. +- `--php-extension-config=`: Load a JSON extension config before PHP starts. Use this for direct `.so` URLs or extension-specific `iniEntries` and `env` settings. Can be used multiple times. +- `--wasm-wordpress-plugin=`: Load a WASM-backed WordPress plugin descriptor. The descriptor loads a PHP.wasm extension and installs an mu-plugin bootstrap that registers WordPress hooks. Can be used multiple times. ### Loading Custom PHP.wasm Extensions @@ -156,6 +158,136 @@ with `extension=` or `zend_extension=` in php.ini: } ``` +Use `--php-extension-config` when the runtime settings should live outside the +manifest, or when loading a direct `.so` URL: + +```json +{ + "name": "sqlite_markdown", + "source": { + "format": "url", + "url": "./dist/sqlite_markdown-php8.4-jspi.so" + }, + "loadWithIniDirective": false +} +``` + +```bash +npx @wp-playground/cli@latest server --php-extension-config=./spx.json +``` + +### Loading WASM-backed WordPress Plugins + +A WASM-backed WordPress plugin is a PHP.wasm extension plus a small PHP +bootstrap. The extension is loaded before PHP starts. The bootstrap is installed +as an mu-plugin after WordPress is available, so it can call WordPress APIs such +as `add_action()` and `add_filter()`. + +Create a project with this shape: + +```text +hello-wasm/ +|-- bootstrap.php +|-- extension/ +| |-- config.m4 +| `-- hello_wasm.c +`-- wasm-wordpress-plugin.json +``` + +Build the native layer with `@php-wasm/compile-extension`: + +```bash +cd hello-wasm +npx @php-wasm/compile-extension \ + --source ./extension \ + --name hello_wasm \ + --php-versions 8.4 \ + --out ./dist +``` + +The C extension should expose ordinary PHP functions or classes: + +```c +PHP_FUNCTION(hello_wasm_render_text) +{ + RETURN_STRING("Hello from WASM"); +} +``` + +The PHP bootstrap can wrap those functions and use WordPress APIs: + +```php +

%s

', + esc_html( hello_wasm_render_text() ) + ); +} +``` + +Finally, create a descriptor: + +```json +{ + "slug": "hello-wasm", + "name": "Hello WASM", + "extension": { + "name": "hello_wasm", + "source": { + "format": "manifest", + "manifestUrl": "./dist/hello-wasm/manifest.json" + } + }, + "bootstrap": "./bootstrap.php", + "hooks": [ + { + "type": "action", + "hook": "admin_notices", + "callback": "hello_wasm_admin_notice" + } + ] +} +``` + +Then run: + +```bash +npx @wp-playground/cli@latest server \ + --php=8.4 \ + --wasm-wordpress-plugin=./hello-wasm.json +``` + +`bootstrap` paths and local extension manifest paths are resolved relative to +the descriptor file. `callback` must name a PHP callable provided by the WASM +extension or by the bootstrap file. + +A complete Hello Dolly-style example lives in +`packages/php-wasm/compile-extension/examples/hello-dolly-wasm`. + +### Editing Markdown Directories + +The `edit-markdown` command opens a directory of Markdown files in wp-admin and +writes block editor saves back to disk: + +```bash +npx @wp-playground/cli@latest edit-markdown ./content +``` + +It loads the Markdown Editor runtime published by +[`adamziel/wp-extensions`](https://github.com/adamziel/wp-extensions), mounts +the Markdown directory at `/markdown-root`, and installs the released +mu-plugin that maps `wp_posts` and `wp_postmeta` to writable SQLite virtual +tables. The current release ships its `sqlite_markdown` extension for PHP 8.4, +so `edit-markdown` defaults to PHP 8.4 and rejects other PHP versions. + +When running from a Playground source checkout, download the runtime artifacts +first: + +```bash +npx nx run playground-cli:download-edit-markdown-runtime +``` + ## Need some help with the CLI? With the Playground CLI, you can use the `--help` to get some support about the available commands. diff --git a/packages/playground/cli/bin/download-edit-markdown-runtime.sh b/packages/playground/cli/bin/download-edit-markdown-runtime.sh new file mode 100755 index 00000000000..56d02107ef5 --- /dev/null +++ b/packages/playground/cli/bin/download-edit-markdown-runtime.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +set -euo pipefail + +PACKAGES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)" +EDIT_MARKDOWN_DIR="$PACKAGES_DIR/playground/cli/src/edit-markdown" +ZIP_URL="https://github.com/adamziel/wp-extensions/releases/download/markdown-editor-latest/wp-markdown-editor.zip" +ZIP_PATH="$EDIT_MARKDOWN_DIR/.tmp/wp-markdown-editor.zip" +RUNTIME_DIR="$EDIT_MARKDOWN_DIR/wp-markdown-editor" + +rm -rf "$EDIT_MARKDOWN_DIR/.tmp" "$RUNTIME_DIR" +mkdir -p "$EDIT_MARKDOWN_DIR/.tmp" + +curl -fsSL "$ZIP_URL" -o "$ZIP_PATH" +unzip -q "$ZIP_PATH" -d "$EDIT_MARKDOWN_DIR" +rm -rf "$EDIT_MARKDOWN_DIR/.tmp" + +test -f "$RUNTIME_DIR/markdown-editor/sqlite-markdown-extension/dist/manifest.json" +test -f "$RUNTIME_DIR/markdown-editor/edit-markdown-mu-plugin.php" diff --git a/packages/playground/cli/project.json b/packages/playground/cli/project.json index 3109c269019..5a430eb172b 100644 --- a/packages/playground/cli/project.json +++ b/packages/playground/cli/project.json @@ -24,7 +24,8 @@ "tsConfig": "packages/playground/cli/tsconfig.lib.json", "outputPath": "dist/packages/playground/cli", "buildTarget": "playground-cli:build:bundle:production" - } + }, + "dependsOn": ["download-edit-markdown-runtime"] }, "build:bundle": { "executor": "@nx/vite:build", @@ -33,6 +34,7 @@ "main": "dist/packages/playground/cli/src/cli.js", "outputPath": "dist/packages/playground/cli" }, + "dependsOn": ["download-edit-markdown-runtime"], "defaultConfiguration": "production", "configurations": { "development": { @@ -43,6 +45,14 @@ } } }, + "download-edit-markdown-runtime": { + "executor": "nx:run-commands", + "cache": false, + "outputs": ["{projectRoot}/src/edit-markdown/wp-markdown-editor"], + "options": { + "command": "bash packages/playground/cli/bin/download-edit-markdown-runtime.sh" + } + }, "dev": { "executor": "nx:run-commands", "options": { diff --git a/packages/playground/cli/src/edit-markdown/configure.ts b/packages/playground/cli/src/edit-markdown/configure.ts new file mode 100644 index 00000000000..7ff50470b1b --- /dev/null +++ b/packages/playground/cli/src/edit-markdown/configure.ts @@ -0,0 +1,130 @@ +import fs from 'fs'; +import path from 'path'; +import type { RunCLIArgs } from '../run-cli'; + +const MARKDOWN_ROOT_VFS_PATH = '/markdown-root'; +const MARKDOWN_EDITOR_MU_PLUGINS_VFS_PATH = '/wordpress/wp-content/mu-plugins'; +const MARKDOWN_EDITOR_RELEASE_PHP_VERSION: NonNullable = + '8.4'; +const EDIT_MARKDOWN_MODULE_DIR = + typeof __dirname !== 'undefined' ? __dirname : import.meta.dirname; +const MARKDOWN_EDITOR_RUNTIME_HOST_PATH = resolveMarkdownEditorRuntimePath(); +const MARKDOWN_EDITOR_MU_PLUGIN_PATH = resolveMarkdownEditorRuntimeAssetPath( + 'edit-markdown-mu-plugin.php' +); +const SQLITE_MARKDOWN_MANIFEST_PATH = resolveMarkdownEditorRuntimeAssetPath( + 'sqlite-markdown-extension', + 'dist', + 'manifest.json' +); + +function resolveMarkdownEditorRuntimePath(): string { + const candidates = [ + path.resolve( + EDIT_MARKDOWN_MODULE_DIR, + 'wp-markdown-editor', + 'markdown-editor' + ), + path.resolve(EDIT_MARKDOWN_MODULE_DIR, 'edit-markdown'), + path.resolve( + EDIT_MARKDOWN_MODULE_DIR, + 'src', + 'edit-markdown', + 'wp-markdown-editor', + 'markdown-editor' + ), + ]; + return ( + candidates.find((candidate) => fs.existsSync(candidate)) ?? + candidates[0] + ); +} + +function resolveMarkdownEditorRuntimeAssetPath(...segments: string[]): string { + return path.resolve(MARKDOWN_EDITOR_RUNTIME_HOST_PATH, ...segments); +} + +function ensureMarkdownEditorRuntime(): void { + if ( + fs.existsSync(SQLITE_MARKDOWN_MANIFEST_PATH) && + fs.existsSync(MARKDOWN_EDITOR_MU_PLUGIN_PATH) + ) { + return; + } + throw new Error( + `edit-markdown: Markdown Editor runtime is missing at ${MARKDOWN_EDITOR_RUNTIME_HOST_PATH}. ` + + 'Run `npx nx run playground-cli:download-edit-markdown-runtime` from the repo root.' + ); +} + +/** + * Rewrite `wp-playground edit-markdown ` into a `start` invocation + * with the bits the markdown editor needs already wired up. + * + * Same shape as `expandStartCommandArgs` in run-cli.ts: take the parsed + * args, change the command, populate the extra mounts / blueprint steps / + * runtime flags the higher-level command needs, and return — the rest of + * runCLI() then runs as if the user had invoked `start` themselves. + * + * What it sets: + * - login=true so the editor opens authenticated. + * - phpExtension=[sqlite-markdown manifest] so PHP registers the + * markdown_posts / markdown_postmeta virtual tables before WordPress + * opens its SQLite database connection. + * - mount of the host markdown directory at {@see MARKDOWN_ROOT_VFS_PATH}. + * - mount of the released Markdown Editor runtime as wp-content/mu-plugins. + */ +export function expandEditMarkdownCommandArgs( + args: RunCLIArgs & { reset?: boolean } +): RunCLIArgs { + const hostDir = (args as any).dir as string; + if (!hostDir) { + throw new Error('edit-markdown: missing required argument.'); + } + const resolved = path.resolve(process.cwd(), hostDir); + if (!fs.existsSync(resolved) || !fs.statSync(resolved).isDirectory()) { + throw new Error( + `edit-markdown: "${hostDir}" is not a readable directory.` + ); + } + + if ( + args.php !== undefined && + args.php !== MARKDOWN_EDITOR_RELEASE_PHP_VERSION + ) { + throw new Error( + `edit-markdown currently requires PHP ${MARKDOWN_EDITOR_RELEASE_PHP_VERSION}. ` + + 'The wp-extensions Markdown Editor release only ships that sqlite_markdown build.' + ); + } + + ensureMarkdownEditorRuntime(); + + const mounts = [ + ...(args.mount || []), + { hostPath: resolved, vfsPath: MARKDOWN_ROOT_VFS_PATH }, + { + hostPath: MARKDOWN_EDITOR_RUNTIME_HOST_PATH, + vfsPath: MARKDOWN_EDITOR_MU_PLUGINS_VFS_PATH, + }, + ]; + + return { + ...args, + login: true, + php: args.php ?? MARKDOWN_EDITOR_RELEASE_PHP_VERSION, + phpExtension: [ + ...(((args as any).phpExtension as string[] | undefined) || []), + SQLITE_MARKDOWN_MANIFEST_PATH, + ], + mount: mounts, + }; +} + +export { + MARKDOWN_ROOT_VFS_PATH, + MARKDOWN_EDITOR_MU_PLUGINS_VFS_PATH, + MARKDOWN_EDITOR_RELEASE_PHP_VERSION, + MARKDOWN_EDITOR_RUNTIME_HOST_PATH, + SQLITE_MARKDOWN_MANIFEST_PATH, +}; diff --git a/packages/playground/cli/src/index.ts b/packages/playground/cli/src/index.ts index 080825f5944..a26f02fcdcd 100644 --- a/packages/playground/cli/src/index.ts +++ b/packages/playground/cli/src/index.ts @@ -1 +1,2 @@ export * from './run-cli'; +export * from './wasm-wordpress-plugins'; diff --git a/packages/playground/cli/src/php-extensions.ts b/packages/playground/cli/src/php-extensions.ts index a2727e6591b..083cc97279b 100644 --- a/packages/playground/cli/src/php-extensions.ts +++ b/packages/playground/cli/src/php-extensions.ts @@ -1,11 +1,16 @@ -import type { PHPExtension, XdebugOptions } from '@php-wasm/node'; +import { readFileSync } from 'node:fs'; +import type { + PHPExtension, + RuntimePHPExtensionSource, + XdebugOptions, +} from '@php-wasm/node'; /** * Converts Playground CLI extension options into the runtime `extensions` * array. * * The CLI receives built-in extensions as individual options (`intl`, `redis`, - * `memcached`, and `xdebug`) and external extensions as manifest paths. + * `memcached`, and `xdebug`) and external extensions as manifest/config paths. * The PHP runtime expects one array that can contain built-in names and * external extension sources side by side. * @@ -18,7 +23,11 @@ export function cliExtensionArgsToExtensionsArray(args: { redis?: boolean; memcached?: boolean; xdebug?: boolean | XdebugOptions; + runtimePHPExtensions?: RuntimePHPExtensionSource[]; phpExtension?: string[]; + 'php-extension'?: string[]; + phpExtensionConfig?: string[]; + 'php-extension-config'?: string[]; }): PHPExtension[] { const extensions: PHPExtension[] = []; if (args.intl) { @@ -37,7 +46,7 @@ export function cliExtensionArgsToExtensionsArray(args: { : 'xdebug' ); } - for (const manifestUrl of args.phpExtension || []) { + for (const manifestUrl of getArrayOption(args, 'phpExtension')) { extensions.push({ source: { format: 'manifest', @@ -45,5 +54,92 @@ export function cliExtensionArgsToExtensionsArray(args: { }, }); } + for (const configPath of getArrayOption(args, 'phpExtensionConfig')) { + extensions.push(readPHPExtensionConfig(configPath)); + } + extensions.push(...(args.runtimePHPExtensions ?? [])); return extensions; } + +export function readPHPExtensionConfig( + configPath: string +): RuntimePHPExtensionSource { + let config: unknown; + try { + config = JSON.parse(readFileSync(configPath, 'utf8')); + } catch (error) { + throw new Error(`Could not read PHP extension config: ${configPath}`, { + cause: error, + }); + } + + if (!isRecord(config) || !isRecord(config['source'])) { + throw new Error( + `Invalid PHP extension config: ${configPath}. Expected an object with a source field.` + ); + } + if ( + 'loadWithIniDirective' in config && + config['loadWithIniDirective'] !== false && + config['loadWithIniDirective'] !== 'extension' && + config['loadWithIniDirective'] !== 'zend_extension' + ) { + throw new Error( + `Invalid PHP extension config: ${configPath}. loadWithIniDirective must be "extension", "zend_extension", or false.` + ); + } + + const source = config['source']; + if (source['format'] === 'so') { + throw new Error( + `Invalid PHP extension config: ${configPath}. The CLI cannot load direct bytes; use a manifest or URL source.` + ); + } + if (source['format'] === 'url') { + if (typeof source['url'] !== 'string') { + throw new Error( + `Invalid PHP extension config: ${configPath}. A URL source requires a string url.` + ); + } + return config as RuntimePHPExtensionSource; + } + if (source['format'] === 'manifest') { + if ( + typeof source['manifestUrl'] !== 'string' && + !isRecord(source['manifest']) + ) { + throw new Error( + `Invalid PHP extension config: ${configPath}. A manifest source requires manifestUrl or manifest.` + ); + } + return config as RuntimePHPExtensionSource; + } + + throw new Error( + `Invalid PHP extension config: ${configPath}. Unknown source format.` + ); +} + +function getArrayOption( + args: { + phpExtension?: string[]; + 'php-extension'?: string[]; + phpExtensionConfig?: string[]; + 'php-extension-config'?: string[]; + }, + camelCaseKey: 'phpExtension' | 'phpExtensionConfig' +): string[] { + const dashCaseKey = + camelCaseKey === 'phpExtension' + ? 'php-extension' + : 'php-extension-config'; + const value = args[camelCaseKey] ?? args[dashCaseKey]; + if (value === undefined) { + return []; + } + return Array.isArray(value) ? value : [value]; +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/playground/cli/src/run-cli.ts b/packages/playground/cli/src/run-cli.ts index 33476d5de75..3ebdc5aa20d 100644 --- a/packages/playground/cli/src/run-cli.ts +++ b/packages/playground/cli/src/run-cli.ts @@ -35,6 +35,8 @@ import { parseMountDirArguments, parseMountWithDelimiterArguments, } from './mounts'; +import { expandEditMarkdownCommandArgs } from './edit-markdown/configure'; +import { expandWasmWordPressPluginArgs } from './wasm-wordpress-plugins'; import { parseDefineStringArguments, parseDefineBoolArguments, @@ -43,7 +45,7 @@ import { import { isPortInUse, startServer } from './start-server'; import type { PlaygroundCliBlueprintV1Worker } from './blueprints-v1/worker-thread-v1'; import type { PlaygroundCliBlueprintV2Worker } from './blueprints-v2/worker-thread-v2'; -import type { XdebugOptions } from '@php-wasm/node'; +import type { RuntimePHPExtensionSource, XdebugOptions } from '@php-wasm/node'; /* eslint-disable no-console */ import { AllPHPVersions, @@ -306,6 +308,20 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { string: true, nargs: 1, }, + 'php-extension-config': { + describe: + 'Load a JSON PHP.wasm extension config before PHP starts. Use this for direct .so URLs or extension-specific ini/env settings. Can be used multiple times.', + type: 'array', + string: true, + nargs: 1, + }, + 'wasm-wordpress-plugin': { + describe: + 'Load a WASM-backed WordPress plugin descriptor. The descriptor loads a PHP.wasm extension and installs an mu-plugin bootstrap that registers WordPress hooks. Can be used multiple times.', + type: 'array', + string: true, + nargs: 1, + }, 'experimental-unsafe-ide-integration': { describe: 'Enable experimental IDE development tools. This option edits IDE config files ' + @@ -430,6 +446,7 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { default: false, }, 'php-extension': sharedOptions['php-extension'], + 'php-extension-config': sharedOptions['php-extension-config'], 'experimental-unsafe-ide-integration': sharedOptions['experimental-unsafe-ide-integration'], 'skip-browser': { @@ -532,6 +549,18 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { .command('php', 'Run a PHP script', (yargsInstance: Argv) => yargsInstance.options({ ...sharedOptions }) ) + .command( + 'edit-markdown ', + 'Open a directory of Markdown files in the block editor', + (yargsInstance: Argv) => + yargsInstance + .positional('dir', { + describe: 'Directory tree of .md files to mount.', + type: 'string', + demandOption: true, + }) + .options(startCommandOptions) + ) .demandCommand(1, 'Please specify a command') .strictCommands() .conflicts( @@ -689,6 +718,7 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { 'server', 'build-snapshot', 'php', + 'edit-markdown', ].includes(command) ) { yargsObject.showHelp(); @@ -727,7 +757,7 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { } } - const cliArgs = { + let cliArgs = { ...args, define, command, @@ -741,6 +771,13 @@ export async function parseOptionsAndRunCLI(argsToParse: string[]) { ], } as RunCLIArgs; + if (command === 'edit-markdown') { + cliArgs = expandEditMarkdownCommandArgs( + cliArgs as RunCLIArgs & { reset?: boolean } + ); + } + cliArgs = expandWasmWordPressPluginArgs(cliArgs); + const cliServer = await runCLI(cliArgs); if (cliServer === undefined) { // No server was started, so we are done with our work. @@ -876,7 +913,13 @@ export interface RunCLIArgs { | BlueprintV1Declaration | BlueprintV2Declaration | BlueprintBundle; - command: 'start' | 'server' | 'run-blueprint' | 'build-snapshot' | 'php'; + command: + | 'start' + | 'server' + | 'run-blueprint' + | 'build-snapshot' + | 'php' + | 'edit-markdown'; debug?: boolean; login?: boolean; mount?: Mount[]; @@ -905,6 +948,10 @@ export interface RunCLIArgs { memcached?: boolean; xdebug?: boolean | XdebugOptions; phpExtension?: string[]; + phpExtensionConfig?: string[]; + wasmWordPressPlugin?: string[]; + 'wasm-wordpress-plugin'?: string[]; + runtimePHPExtensions?: RuntimePHPExtensionSource[]; experimentalUnsafeIdeIntegration?: string[]; experimentalDevtools?: boolean; 'experimental-blueprints-v2-runner'?: boolean; @@ -1006,6 +1053,8 @@ export async function runCLI(args: RunCLIArgs): Promise { verbosity: args.verbosity || 'normal', }); + args = expandWasmWordPressPluginArgs(args); + if (args.command === 'start') { args = expandStartCommandArgs(args, cliOutput); } @@ -1800,6 +1849,14 @@ export async function runCLI(args: RunCLIArgs): Promise { if (server && args.command === 'start' && !args.skipBrowser) { openInBrowser(server.serverUrl); } + if (server && args.command === 'edit-markdown' && !args.skipBrowser) { + openInBrowser( + new URL( + '/wp-admin/edit.php?post_type=page', + server.serverUrl + ).toString() + ); + } return server; } diff --git a/packages/playground/cli/src/wasm-wordpress-plugins.ts b/packages/playground/cli/src/wasm-wordpress-plugins.ts new file mode 100644 index 00000000000..8b20bc91904 --- /dev/null +++ b/packages/playground/cli/src/wasm-wordpress-plugins.ts @@ -0,0 +1,404 @@ +import { readFileSync } from 'node:fs'; +import path from 'node:path'; +import type { RuntimePHPExtensionSource } from '@php-wasm/node'; +import type { RunCLIArgs } from './run-cli'; + +const MU_PLUGINS_VFS_DIR = '/wordpress/wp-content/mu-plugins'; + +export interface WasmWordPressPluginHook { + type: 'action' | 'filter'; + hook: string; + callback: string; + priority?: number; + acceptedArgs?: number; +} + +export interface WasmWordPressPluginConfig { + /** + * Stable WordPress plugin slug. Used for the generated mu-plugin filename. + */ + slug: string; + name?: string; + description?: string; + version?: string; + /** + * PHP.wasm extension that exposes PHP functions/classes implemented by + * the WebAssembly side module. + */ + extension: RuntimePHPExtensionSource; + /** + * Optional PHP bootstrap file. Relative paths resolve from the descriptor + * file. Use this for PHP wrappers around extension functions. + */ + bootstrap?: string; + /** + * Inline PHP bootstrap code. Mutually exclusive with `bootstrap`. + */ + bootstrapCode?: string; + /** + * Declarative WordPress hook registrations. The callback must name a PHP + * callable made available by the extension or bootstrap code. + */ + hooks?: WasmWordPressPluginHook[]; +} + +export function expandWasmWordPressPluginArgs(args: RunCLIArgs): RunCLIArgs { + const configPaths = getArrayOption(args, 'wasmWordPressPlugin'); + if (!configPaths.length) { + return args; + } + + const pluginConfigs = configPaths.map(readWasmWordPressPluginConfig); + const runtimePHPExtensions = [ + ...(args.runtimePHPExtensions ?? []), + ...pluginConfigs.map((plugin) => plugin.extension), + ]; + const extraSteps = [ + ...((args as any)['additional-blueprint-steps'] || []), + ...pluginConfigs.map((plugin) => ({ + step: 'writeFile', + path: `${MU_PLUGINS_VFS_DIR}/${plugin.slug}.php`, + data: createWasmWordPressPluginBootstrap(plugin), + })), + ]; + + return { + ...args, + wasmWordPressPlugin: undefined, + 'wasm-wordpress-plugin': undefined, + runtimePHPExtensions, + 'additional-blueprint-steps': extraSteps, + }; +} + +export function readWasmWordPressPluginConfig( + configPath: string +): WasmWordPressPluginConfig { + const absoluteConfigPath = path.resolve(process.cwd(), configPath); + const configDir = path.dirname(absoluteConfigPath); + let config: unknown; + try { + config = JSON.parse(readFileSync(absoluteConfigPath, 'utf8')); + } catch (error) { + throw new Error(`Could not read WASM WordPress plugin: ${configPath}`, { + cause: error, + }); + } + + if (!isRecord(config)) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. Expected an object.` + ); + } + + const slug = config['slug']; + if (typeof slug !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(slug)) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. slug must contain lowercase letters, numbers, and hyphens.` + ); + } + + if (!isRecord(config['extension'])) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. Expected an extension object.` + ); + } + + const bootstrap = config['bootstrap']; + const bootstrapCode = config['bootstrapCode']; + if (bootstrap !== undefined && typeof bootstrap !== 'string') { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. bootstrap must be a string path.` + ); + } + if (bootstrapCode !== undefined && typeof bootstrapCode !== 'string') { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. bootstrapCode must be a string.` + ); + } + if (bootstrap !== undefined && bootstrapCode !== undefined) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. Use bootstrap or bootstrapCode, not both.` + ); + } + + const hooks = readHooks(configPath, config['hooks']); + const inlineBootstrap = + typeof bootstrapCode === 'string' + ? bootstrapCode + : bootstrap + ? readBootstrap(configPath, configDir, bootstrap) + : undefined; + + return { + slug, + name: readOptionalString(configPath, config, 'name'), + description: readOptionalString(configPath, config, 'description'), + version: readOptionalString(configPath, config, 'version'), + extension: normalizeExtensionConfig( + configPath, + configDir, + config['extension'] + ), + bootstrapCode: inlineBootstrap, + hooks, + }; +} + +export function createWasmWordPressPluginBootstrap( + plugin: WasmWordPressPluginConfig +): string { + const lines = [ + ' +): RuntimePHPExtensionSource { + assertRuntimePHPExtensionSource(configPath, extension); + const normalized = structuredClone(extension) as RuntimePHPExtensionSource; + const source = normalized.source as Record; + if (source['format'] === 'url') { + source['url'] = resolveLocalReference(configDir, source['url']); + } + if (source['format'] === 'manifest') { + if (source['manifestUrl'] !== undefined) { + source['manifestUrl'] = resolveLocalReference( + configDir, + source['manifestUrl'] + ); + } + if (source['baseUrl'] !== undefined) { + source['baseUrl'] = resolveLocalReference( + configDir, + source['baseUrl'] + ); + } + } + return normalized; +} + +function assertRuntimePHPExtensionSource( + configPath: string, + extension: Record +): void { + if (!isRecord(extension['source'])) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. extension.source is required.` + ); + } + if ( + 'loadWithIniDirective' in extension && + extension['loadWithIniDirective'] !== false && + extension['loadWithIniDirective'] !== 'extension' && + extension['loadWithIniDirective'] !== 'zend_extension' + ) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. extension.loadWithIniDirective must be "extension", "zend_extension", or false.` + ); + } + + const source = extension['source']; + if (source['format'] === 'so') { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. The CLI cannot load direct bytes; use a manifest or URL source.` + ); + } + if (source['format'] === 'url') { + if (typeof source['url'] !== 'string') { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. A URL source requires a string url.` + ); + } + return; + } + if (source['format'] === 'manifest') { + if ( + typeof source['manifestUrl'] !== 'string' && + !isRecord(source['manifest']) + ) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. A manifest source requires manifestUrl or manifest.` + ); + } + return; + } + + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. Unknown extension source format.` + ); +} + +function readHooks( + configPath: string, + hooks: unknown +): WasmWordPressPluginHook[] { + if (hooks === undefined) { + return []; + } + if (!Array.isArray(hooks)) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. hooks must be an array.` + ); + } + return hooks.map((hook, index) => { + if (!isRecord(hook)) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. hooks[${index}] must be an object.` + ); + } + const type = hook['type']; + if (type !== 'action' && type !== 'filter') { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. hooks[${index}].type must be "action" or "filter".` + ); + } + if (typeof hook['hook'] !== 'string') { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. hooks[${index}].hook must be a string.` + ); + } + if (typeof hook['callback'] !== 'string') { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. hooks[${index}].callback must be a string.` + ); + } + return { + type, + hook: hook['hook'], + callback: hook['callback'], + priority: readOptionalNumber(configPath, hook, 'priority'), + acceptedArgs: readOptionalNumber(configPath, hook, 'acceptedArgs'), + }; + }); +} + +function readBootstrap( + configPath: string, + configDir: string, + bootstrap: string +): string { + try { + return readFileSync(path.resolve(configDir, bootstrap), 'utf8'); + } catch (error) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. Could not read bootstrap file ${bootstrap}.`, + { cause: error } + ); + } +} + +function readOptionalString( + configPath: string, + record: Record, + key: string +): string | undefined { + const value = record[key]; + if (value === undefined) { + return undefined; + } + if (typeof value !== 'string') { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. ${key} must be a string.` + ); + } + return value; +} + +function readOptionalNumber( + configPath: string, + record: Record, + key: string +): number | undefined { + const value = record[key]; + if (value === undefined) { + return undefined; + } + if (typeof value !== 'number' || !Number.isInteger(value) || value < 0) { + throw new Error( + `Invalid WASM WordPress plugin: ${configPath}. ${key} must be a non-negative integer.` + ); + } + return value; +} + +function resolveLocalReference(configDir: string, value: unknown): unknown { + if (typeof value !== 'string' || isRemoteOrFileUrl(value)) { + return value; + } + return path.resolve(configDir, value); +} + +function isRemoteOrFileUrl(value: string): boolean { + return /^(https?:|file:)/.test(value); +} + +function getArrayOption( + args: RunCLIArgs, + camelCaseKey: 'wasmWordPressPlugin' +): string[] { + const dashCaseKey = 'wasm-wordpress-plugin'; + const optionSource = args as unknown as Record; + const value = optionSource[camelCaseKey] ?? optionSource[dashCaseKey]; + if (value === undefined) { + return []; + } + return Array.isArray(value) ? (value as string[]) : [value as string]; +} + +function phpStringLiteral(value: string): string { + return `'${value.replace(/\\/g, '\\\\').replace(/'/g, "\\'")}'`; +} + +function trimPhpTags(source: string): string { + return source.replace(/^\s*<\?php\s*/u, '').replace(/\s*\?>\s*$/u, ''); +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/packages/playground/cli/tests/php-extensions.spec.ts b/packages/playground/cli/tests/php-extensions.spec.ts index 3d20657bb16..36b4910f771 100644 --- a/packages/playground/cli/tests/php-extensions.spec.ts +++ b/packages/playground/cli/tests/php-extensions.spec.ts @@ -1,4 +1,10 @@ -import { cliExtensionArgsToExtensionsArray } from '../src/php-extensions'; +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { tmpdir } from 'node:os'; +import { + cliExtensionArgsToExtensionsArray, + readPHPExtensionConfig, +} from '../src/php-extensions'; describe('CLI PHP extensions', () => { test('converts built-in extension flags to runtime extension requests', () => { @@ -35,4 +41,52 @@ describe('CLI PHP extensions', () => { }, ]); }); + + test('reads --php-extension-config JSON files', async () => { + const tempDir = await mkdtemp(path.join(tmpdir(), 'php-extension-')); + const configPath = path.join(tempDir, 'extension.json'); + await writeFile( + configPath, + JSON.stringify({ + name: 'sqlite_markdown', + source: { + format: 'url', + url: './dist/sqlite_markdown-php8.4-jspi.so', + }, + loadWithIniDirective: false, + }) + ); + + try { + expect(readPHPExtensionConfig(configPath)).toEqual({ + name: 'sqlite_markdown', + source: { + format: 'url', + url: './dist/sqlite_markdown-php8.4-jspi.so', + }, + loadWithIniDirective: false, + }); + expect( + cliExtensionArgsToExtensionsArray({ + phpExtensionConfig: [configPath], + }) + ).toEqual([readPHPExtensionConfig(configPath)]); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); + + test('rejects config files without an external extension source', async () => { + const tempDir = await mkdtemp(path.join(tmpdir(), 'php-extension-')); + const configPath = path.join(tempDir, 'extension.json'); + await writeFile(configPath, JSON.stringify({ name: 'broken' })); + + try { + expect(() => readPHPExtensionConfig(configPath)).toThrow( + 'Expected an object with a source field' + ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); }); diff --git a/packages/playground/cli/tests/wasm-wordpress-plugins.spec.ts b/packages/playground/cli/tests/wasm-wordpress-plugins.spec.ts new file mode 100644 index 00000000000..1b35f867356 --- /dev/null +++ b/packages/playground/cli/tests/wasm-wordpress-plugins.spec.ts @@ -0,0 +1,177 @@ +import { mkdtemp, rm, writeFile } from 'node:fs/promises'; +import path from 'node:path'; +import { tmpdir } from 'node:os'; +import { cliExtensionArgsToExtensionsArray } from '../src/php-extensions'; +import { + createWasmWordPressPluginBootstrap, + expandWasmWordPressPluginArgs, + readWasmWordPressPluginConfig, +} from '../src/wasm-wordpress-plugins'; + +describe('WASM WordPress plugins', () => { + test('reads the bundled Hello Dolly WASM example descriptor', () => { + const exampleConfigPath = path.resolve( + import.meta.dirname, + '../../../php-wasm/compile-extension/examples/hello-dolly-wasm/wasm-wordpress-plugin.json' + ); + + const config = readWasmWordPressPluginConfig(exampleConfigPath); + + expect(config.slug).toBe('hello-dolly-wasm'); + expect(config.extension).toEqual({ + name: 'hello_dolly_wasm', + source: { + format: 'manifest', + manifestUrl: path.resolve( + path.dirname(exampleConfigPath), + './dist/manifest.json' + ), + }, + }); + expect(config.hooks).toEqual([ + { + type: 'action', + hook: 'admin_notices', + callback: 'hello_dolly_wasm_render', + priority: undefined, + acceptedArgs: undefined, + }, + { + type: 'action', + hook: 'admin_head', + callback: 'hello_dolly_wasm_css', + priority: undefined, + acceptedArgs: undefined, + }, + ]); + expect(config.bootstrapCode).toContain( + 'function hello_dolly_wasm_render' + ); + }); + + test('expands a plugin descriptor into runtime extension and mu-plugin bootstrap', async () => { + const tempDir = await mkdtemp(path.join(tmpdir(), 'wasm-wp-plugin-')); + const bootstrapPath = path.join(tempDir, 'bootstrap.php'); + const configPath = path.join(tempDir, 'plugin.json'); + await writeFile( + bootstrapPath, + ` { + const bootstrap = createWasmWordPressPluginBootstrap({ + slug: 'cache-warmer', + name: 'Cache Warmer', + extension: { + name: 'cache_warmer', + source: { + format: 'manifest', + manifestUrl: '/tmp/cache-warmer/manifest.json', + }, + }, + bootstrapCode: 'function cache_warmer_boot() {}', + hooks: [ + { + type: 'action', + hook: 'init', + callback: 'cache_warmer_boot', + }, + ], + }); + + expect(bootstrap).toContain("extension_loaded( 'cache_warmer' )"); + expect(bootstrap).toContain('function cache_warmer_boot() {}'); + expect(bootstrap).toContain( + "add_action( 'init', 'cache_warmer_boot', 10, 1 );" + ); + }); + + test('rejects descriptors without a valid slug', async () => { + const tempDir = await mkdtemp(path.join(tmpdir(), 'wasm-wp-plugin-')); + const configPath = path.join(tempDir, 'plugin.json'); + await writeFile( + configPath, + JSON.stringify({ + slug: 'Not Valid', + extension: { + source: { + format: 'manifest', + manifestUrl: './manifest.json', + }, + }, + }) + ); + + try { + expect(() => readWasmWordPressPluginConfig(configPath)).toThrow( + 'slug must contain lowercase letters, numbers, and hyphens' + ); + } finally { + await rm(tempDir, { recursive: true, force: true }); + } + }); +}); diff --git a/packages/playground/cli/tsconfig.lib.json b/packages/playground/cli/tsconfig.lib.json index f0e3898a270..c06cfeb70c8 100644 --- a/packages/playground/cli/tsconfig.lib.json +++ b/packages/playground/cli/tsconfig.lib.json @@ -12,5 +12,10 @@ "../blueprints/src/lib/v2/run-blueprint-v2.ts", "../blueprints/src/lib/v2/blueprint-v2-declaration.ts" ], - "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"] + "exclude": [ + "jest.config.ts", + "src/**/*.spec.ts", + "src/**/*.test.ts", + "src/edit-markdown/wp-markdown-editor/**" + ] } diff --git a/packages/playground/cli/vite.config.ts b/packages/playground/cli/vite.config.ts index 403599aafbd..56ad6638840 100644 --- a/packages/playground/cli/vite.config.ts +++ b/packages/playground/cli/vite.config.ts @@ -1,5 +1,5 @@ /// -import { copyFileSync } from 'fs'; +import { copyFileSync, cpSync, existsSync } from 'fs'; import { createRequire } from 'module'; import { dirname, join } from 'path'; import { pathToFileURL } from 'url'; @@ -163,6 +163,28 @@ const plugins = [ ); }, }, + { + name: 'copy-edit-markdown-assets-to-output', + + writeBundle(options) { + const outputDir = options.dir; + if (!outputDir) return; + + const runtimePath = join( + __dirname, + 'src', + 'edit-markdown', + 'wp-markdown-editor', + 'markdown-editor' + ); + if (!existsSync(runtimePath)) { + return; + } + cpSync(runtimePath, join(outputDir, 'edit-markdown'), { + recursive: true, + }); + }, + }, ...viteGlobalExtensions, ] as PluginOption[]; diff --git a/packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs b/packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs index 0fdbb1c3f81..fcd453f020e 100644 --- a/packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs +++ b/packages/playground/wordpress/tests/test-legacy-wp-version-boot.mjs @@ -259,7 +259,7 @@ async function waitForPluginActivation( page, previousFrameUrl, previousBody, - timeoutSeconds = 60 + timeoutSeconds = 120 ) { const deadline = Date.now() + timeoutSeconds * 1000; while (Date.now() < deadline) { @@ -674,11 +674,18 @@ for (const { wp, php } of MATRIX) { // --- Phase 5: Plugin activation --- if (adminStatus && adminStatus.status === 'OK') { try { - const wp4 = await navigateViaUrlBar( + let wp4 = await navigateViaUrlBar( page, '/wp-admin/plugins.php', 30 ); + if (!wp4) { + wp4 = await navigateViaUrlBar( + page, + '/wp-admin/plugins.php', + 30 + ); + } if (!wp4) { pluginStatus = { status: 'TIMEOUT' }; } else { @@ -704,7 +711,7 @@ for (const { wp, php } of MATRIX) { try { await anyActivate.waitFor({ state: 'visible', - timeout: 15000, + timeout: 30000, }); } catch {} const helloActivate = wp4.frame @@ -716,17 +723,56 @@ for (const { wp, php } of MATRIX) { ? helloActivate : anyActivate; if ((await activateLink.count()) > 0) { + const activationPath = normalizeAdminHref( + await activateLink.getAttribute('href') + ); const bodyBeforeActivation = await wp4.frame .locator('body') .innerText({ timeout: 2000 }) .catch(() => wp4.body); const prevFrameUrl = wp4.frame.url(); - await activateLink.click({ timeout: 5000 }); - const wp4b = await waitForPluginActivation( + try { + await activateLink.click({ + timeout: 5000, + noWaitAfter: true, + }); + } catch { + if (activationPath) { + await navigateViaUrlBar( + page, + activationPath, + 60 + ); + } + } + let wp4b = await waitForPluginActivation( page, prevFrameUrl, bodyBeforeActivation ); + if (!wp4b && activationPath) { + wp4b = await navigateViaUrlBar( + page, + activationPath, + 60 + ); + } + if (!wp4b) { + const refreshed = await navigateViaUrlBar( + page, + '/wp-admin/plugins.php', + 30 + ); + if (refreshed) { + const helloDeactivate = refreshed.frame + .locator('a[href*="hello.php"]') + .filter({ hasText: 'Deactivate' }) + .first(); + if ((await helloDeactivate.count()) > 0) { + wp4b = refreshed; + } + } + } if (!wp4b) { pluginStatus = { status: 'TIMEOUT' }; } else { @@ -799,6 +845,23 @@ for (const { wp, php } of MATRIX) { await context.close(); } +function normalizeAdminHref(href) { + if (!href) { + return null; + } + if (href.startsWith('/')) { + return href; + } + try { + const url = new URL(href); + return `${url.pathname}${url.search}${url.hash}`; + } catch {} + if (href.startsWith('plugins.php')) { + return `/wp-admin/${href}`; + } + return href; +} + await browser.close(); const PHASES = ['front', 'post', 'admin', 'newPost', 'plugin'];