Skip to content

[CLI] Pin file-locking test suite to 3 workers#3521

Merged
mho22 merged 1 commit into
trunkfrom
fix/file-locking-test-worker-count
Apr 23, 2026
Merged

[CLI] Pin file-locking test suite to 3 workers#3521
mho22 merged 1 commit into
trunkfrom
fix/file-locking-test-worker-count

Conversation

@pento
Copy link
Copy Markdown
Member

@pento pento commented Apr 22, 2026

Motivation for the change, related issues

Follow-up to #3504.

test-playground-cli (macos-latest) started failing consistently after #3504 merged. Every failing run showed the same five timeouts in packages/playground/cli/tests/file-locking.spec.ts:

  • PHP flock() > should grant multiple shared locks on a file
  • PHP flock() > should release a shared lock when its associated file descriptor is closed
  • PHP flock() > should release an exclusive lock when its associated file descriptor is closed
  • PHP flock() > should release a shared lock when the owning process exits
  • PHP flock() > should release an exclusive lock when the owning process exits

Root cause

The multi shared locks test at file-locking.spec.ts:1090 fires three concurrent PHP requests with Promise.all. Each script busy-waits on a coordination file that only the next script in the chain can write, so all three must run concurrently.

Before #3504 the CLI hardcoded 6 workers, which was always enough. After #3504 the default dropped to min(6, max(1, cpus - 1)). GitHub's macos-latest runners report 3 CPUs, resolving to 2 workers — so PHP3 is permanently queued and the test deadlocks.

Why four unrelated tests also fail

The runCLI server is created once in beforeAll and shared across every test in the describe. When the multi-shared test times out, its two workers are still spinning inside PHP-level while (file_get_contents(...) \!== ...) loops — vitest can abort the JS await, but there's no hook to unblock PHP. The four two-script tests that follow multi-shared in file order each do Promise.all([fetchScript(php1), fetchScript(php2)]), inherit a pool with zero free workers, and time out at 60 s each.

Evidence this is the right model:

  • The two-script tests that run before multi-shared pass (deny-exclusive-with-shared, deny-shared-with-exclusive). The two-script tests after fail. Failures aren't interleaved among the earlier ones.
  • Every failure is a 60 s timeout, never an assertion error. Independent flock races would surface as expect(lock_acquired).toBe(false)-style failures.
  • A 200-run audit of test-playground-cli (macos-latest) across all branches shows no consistent failure pattern before [CLI] Add --workers=<n|auto> flag to configure worker thread count #3504 merged.

Implementation details

Pass workers: 3 to runCLI() in the suite's beforeAll. Three is the minimum the multi-shared test needs; anything lower re-introduces the cascade. Explicit integer values bypass the min(6, cpus-1) default clamp (confirmed by the honors an explicit --workers=3 test at run-cli.spec.ts:1886), so the suite is now robust to host CPU count.

No production code changes. Only the test file is touched.

Testing Instructions (or ideally a Blueprint)

# Passes on macOS-like hosts with 3 CPUs:
npx nx test-playground-cli playground-cli --testFile=file-locking.spec.ts

# To reproduce the original failure, temporarily change workers: 3 to workers: 2:
# expect 5 timeouts in the PHP flock() describe.

On CI, the test-playground-cli (macos-latest) job should turn green. Ubuntu and Windows runners were already passing (their default worker count was already ≥3) and should remain so.

AI disclosure

Per WordPress AI Guidelines:

AI assistance: Yes
Tool: Claude Code (Claude Opus 4.7)
Used for: Root-cause analysis of the worker-starvation cascade, drafting the fix and this PR description. All code was reviewed and tested by the author.

The `multi shared locks` test at file-locking.spec.ts:1090 fires three
concurrent PHP requests with `Promise.all`, each busy-waiting on a
coordination file that only the next script in the chain can write.
It therefore needs three PHP workers running concurrently.

Before #3504 the CLI hardcoded 6 workers, which was always enough.
After #3504 the default dropped to `min(6, max(1, cpus - 1))`. On
`macos-latest` GitHub runners (3 CPUs) that resolves to 2 workers,
leaving PHP3 permanently queued and deadlocking the test.

The cascade is worse than a single timeout: the `runCLI` server is
shared across the whole describe via `beforeAll`, and when the
multi-shared test times out its two workers are still spinning
inside PHP-level `while (file_get_contents(...) \!== ...)` loops —
vitest can abort the JS `await`, but there's no hook to unblock PHP.
The four two-script tests that follow in file order then inherit a
pool with zero free workers and each times out at 60 s.

Pin the suite to `workers: 3` so it's robust to CI host CPU counts.
Three is the minimum the multi-shared test needs; anything lower
re-introduces the cascade.

Follow-up to #3504.
@pento pento requested review from a team, Copilot and mho22 April 22, 2026 23:33
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Pins the Playground CLI file-locking test suite’s CLI worker pool size to prevent worker starvation deadlocks on low-CPU CI runners (notably macos-latest).

Changes:

  • Passes an explicit workers: 3 option to the shared runCLI() server created in beforeAll.
  • Documents why fewer than 3 workers leads to deadlock and cascading timeouts in subsequent tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@chubes4
Copy link
Copy Markdown
Contributor

chubes4 commented Apr 23, 2026

Independently reproduced and verified the fix locally. Came at this from the other direction (looking at the remaining intermittent failure on #3494) and landed on the same root cause and the same fix.

Repro on macOS (M-series), same PR-branch-merged-with-trunk tree CI runs against:

workers result
workers: 2 (GitHub macos-latest default after #3504) same 5 tests hang at 60s each, in the same order as CI
workers: 3 (this PR) all 16 pass in ~9s

The cascade you describe matches what I see locally too — the multi shared locks test is the one that actually deadlocks (needs 3 concurrent workers), and the 4 tests that follow in file order inherit a pool with zero free workers because PHP is still spinning inside while (file_get_contents(...) !== ...) with no way for vitest to interrupt it.

Confirms this is independent of #3494; #3494 fixes a different class of concurrency bug (early worker release + dead-worker eviction). 👍 on shipping this.

@pento
Copy link
Copy Markdown
Member Author

pento commented Apr 23, 2026

While looking at the failing test on this PR (should be able to follow external symlinks in primary and secondary PHP instances), I noticed that test is also only flaky under 2 workers. It does become more reliable if pinned to 3 workers, but that doesn't address the underlying causes, which appears to be the same as #3494.

@mho22 mho22 merged commit 7d08b1f into trunk Apr 23, 2026
91 of 93 checks passed
@mho22 mho22 deleted the fix/file-locking-test-worker-count branch April 23, 2026 07:56
@mho22
Copy link
Copy Markdown
Collaborator

mho22 commented Apr 23, 2026

@pento Thanks again for your contribution. @chubes4 Sorry for the delay, I will focus on your pull request today.

@chubes4
Copy link
Copy Markdown
Contributor

chubes4 commented Apr 23, 2026

@mho22 No problem! Thanks for your work

Premiermoney added a commit to Cdult/wordpress-playground-bce20d75 that referenced this pull request Jun 2, 2026
* Refresh SQLite integration plugin

* Define DB_NAME via auto-prepend instead of rewriting wp-config.php (#3458)

## Summary

- Stop modifying `wp-config.php` to insert `define('DB_NAME',
'wordpress')`. Instead, use static token-based parsing to check if the
constant is already defined, and define it via the PHP auto-prepend
script if missing.
- The user's `wp-config.php` value always takes priority — if they
define `DB_NAME`, Playground respects it.
- Extract the logic into a reusable `defineWpConfigConstantFallbacks()`
function.

## Test plan

- [x] Existing tests updated and passing (79 tests in
`wp-config.spec.ts`)
- [ ] Verify fresh Playground boot works (`DB_NAME` defaults to
`database-name-here` from `wp-config-sample.php`)
- [ ] Verify mounting a project with custom `DB_NAME` in `wp-config.php`
uses the custom value
- [ ] Verify importing a WordPress backup without `DB_NAME` in
`wp-config.php` still works (`DB_NAME` is `wordpress`)

Closes https://github.com/WordPress/wordpress-playground/issues/2530.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

* [Website] Add custom error message when artifact is expired (#3453)

## Motivation for the change, related issues

A WordCamp Nice 2026 contributor asked why this was crashing : 

https://playground.wordpress.net/wordpress.html?pr=9026

I found out the CI build artifact for that pull request was expired. To
load that pull request, the author or a maintainer should push a new
commit, rebase or rerun the CI Job to trigger a fresh CI build.


## Implementation details

- Added a new custom error message.

## Testing Instructions (or ideally a Blueprint)


1. `npm run dev`
2. Go to `http://localhost:5400/website-server/wordpress.html?pr=9026`

[On Playground](https://playground.wordpress.net/wordpress.html?pr=9026)

<img width="1917" height="1033" alt="remote-screenshot"
src="https://github.com/user-attachments/assets/d8411c5e-a186-46d9-8c09-4b9c60a7c71a"
/>

[Locally](http://localhost:5400/website-server/wordpress.html?pr=9026)

<img width="1915" height="1036" alt="local-screenshot"
src="https://github.com/user-attachments/assets/b0e2079d-34ab-46a7-b3a3-918ba57918c9"
/>

---------

Co-authored-by: Jan Jakeš <[email protected]>

* [ xdebug ] Skip paths alongside path mappings in IDE configs  (#3366)

## Motivation for the change, related issues

Follow-up on :
https://github.com/WordPress/wordpress-playground/pull/3115

This pull request aims to provide a way to ignore paths from step
debugging. This feature has been enabled internally in Xdebug 3.5 [ PHP
8.5 ] but for older PHP versions, only IDEs can somehow ignore paths.

## Implementation details

#### VSCode 

By adding a `skipFiles` property with an array of skipped paths :

`.vscode/launch.json`

```json
{
    "configurations": [
        {
            "name": "PHP.wasm CLI - Listen for Xdebug",
            "type": "php",
            "request": "launch",
            "port": 9003,
            "skipFiles": [
                "/php/foo.php",
                "/baz/**"
            ],
            "pathMappings": {
                "/": "${workspaceRoot}/",
            }
        }
    ]
}
```

#### PhpStorm :

By creating a new file named `php.xml` which will list skipped files.

`.idea/workspace.xml`

```
<servers>
      <server host="example.com:443" port="80" name="Listen for Xdebug" use_path_mappings="true">
        <path_mappings>
          <mapping local-root="$PROJECT_DIR$" remote-root="/" />
        </path_mappings>
      </server>
</servers>
```

`.idea/php.xml`

```
<component name="PhpStepFilterConfiguration">
    <skipped_files>
      <skipped_file file="$PROJECT_DIR$/php/foo.php" />
      <skipped_file file="$PROJECT_DIR$/baz" />
    </skipped_files>
</component>
```

## Testing Instructions

### Setup

Create the following PHP files in a test directory:

**`php/xdebug.php`**

```php
<?php

echo "In\n";

require_once __DIR__ . "/foo.php";

foo();

echo "Out\n";
```

**`php/foo.php`**

```php
<?php

require_once __DIR__ . "/../../proc/qux.php";

function foo() {
    echo "Foo\n";
    qux();
}
```

**`proc/qux.php`**

```php
<?php

require_once __DIR__ . "/../wordpress/php/bar.php";

function qux() {
    echo "Qux\n";
    bar();
}
```

**`php/bar.php`**

```php
<?php

function bar() {
    echo "Hello Xdebug World!\n";
}
```

### Steps (VSCode)

1. Make sure you have the **PHP Debug** extension installed in VSCode
2. Run `npx nx dev playground-cli server --php=8.4 --xdebug
--experimental-unsafe-ide-integration=vscode --skip-wordpress-install
--mount=./php:/wordpress/php --mount=./proc:/proc`.
3. Navigate to `http://localhost:9400/php/xdebug.php` in the browser.
4. You should see this : `In Foo Qux Hello Xdebug World! Out`.
5. Verify that the **"WP Playground CLI - Listen for Xdebug"**
configuration was created in `.vscode/launch.json` with default
`skipFiles`.
5. Go to `Run` > `Start Debugging` and select **"WP Playground CLI -
Listen for Xdebug"** in the Debug Console.
6. Add a breakpoint in `php/xdebug.php` on `foo()` on **line 7**
7. Reload the `http://localhost:9400/php/xdebug.php` page in the
browser.
8. Witness the magic break.
9. **Step into** and confirm the debugger should enter `foo.php`
normally.
10. **Step into** two times and confirm the debugger should **skip**
`proc/qux.php` (since `/proc/**` is in the default `skipFiles`) and land
directly in `php/bar.php`.
11. It still should display : `In Foo Qux Hello Xdebug World! Out` at
the end of the step debugging.

or

CI

or

`npx nx test php-wasm-cli-util --testFile=xdebug-path-mappings.spec.ts`

* [AI] Add compilation and debugging skills for PHP.wasm (#3445)

## Motivation for the change, related issues

Working with PHP.wasm compilation and debugging requires deep knowledge
of Emscripten flags, Asyncify/JSPI mechanics, dynamic linking, and
WASM-specific error patterns. This knowledge was previously scattered or
undocumented, making it difficult for AI agents to efficiently diagnose
and fix PHP.wasm issues.

These three skills codify the patterns discovered through extensive
debugging work across dynamic extension loading , Emscripten upgrades,
WASM memory growth bugs, and Asyncify crash resolution.

## Implementation details

Adds three new Claude Code skills in .agents/skills/:

- `compile-php-wasm` : Reference for compiling PHP.wasm main modules and
side modules. Covers the build pipeline, Emscripten flags, `MAIN_MODULE`
linking gotchas, side module compilation with `libtool` workarounds,
Emscripten
  version upgrade checklist, cache busting, and WASM binary inspection.
- `debug-php-wasm-main-module` : Reference for debugging crashes in the
main PHP.wasm binary. Covers Asyncify error interpretation, step-by-step
Asyncify crash debugging strategy, JSPI suspension errors, WASM memory
growth bugs, PHP startup lifecycle, WASM-JS boundary tracing, and test
infrastructure gotchas.
- `debug-php-wasm-side-modules` : Reference for debugging dynamic PHP
extensions (WASM side modules). Covers `_dlopen_js` synchronous
override, JSPI suspension patterns for side modules, extension loading
lifecycle, `ASYNCIFY_IMPORTS` requirements, Asyncify shared globals, C++
weak symbol crashes, web platform loading, and version coupling.

Also trims the root `AGENTS.md` to remove PHP recompilation details now
covered by the skills, replacing them with pointers to the relevant
skill names.

---------

Co-authored-by: Bero <[email protected]>

* Refresh SQLite integration plugin

* Refresh SQLite integration plugin

* exclude loopback requests from trying to pre-fetch (#3305)

## Motivation for the change, related issues

Under special cron conditions (WordPress plugin that wants to be
aggressive about triggering cron by setting a cron event that's not in
future, hence already due), loopback url was caught inside of pre-filter
check, which would compete for an existing php worker instance, making
the user initiated request wait up, leading to poor experience.

This PR prevents that loopback call from happening.

## Implementation details
In the context of prefetching update checks' requests, we filter out the
self loopback request

## Testing Instructions (or ideally a Blueprint)

The added E2E test would fail if you were to apply this patch (meaning
self loopback request wasn't halted prior to `post_message_to_js()`
call:

```
diff --git a/packages/playground/remote/src/lib/wordpress-fetch-network-transport.ts b/packages/playground/remote/src/lib/wordpress-fetch-network-transport.ts
index 9fb909994..be79decdb 100644
--- a/packages/playground/remote/src/lib/wordpress-fetch-network-transport.ts
+++ b/packages/playground/remote/src/lib/wordpress-fetch-network-transport.ts
@@ -191,6 +191,7 @@ export class WordPressFetchNetworkTransport {
                                require_once '/wordpress/wp-admin/includes/dashboard.php';

                                function _wppg_is_loopback_request( $url ) {
+                                       return false;
                                        $parsed_url_req  = wp_parse_url( $url );
                                        $parsed_site_url = wp_parse_url( site_url() );
                                        if ( ! is_array( $parsed_url_req ) || ! is_array( $parsed_site_url ) ) {
``` 

Run the test by:

```
source ~/.nvm/nvm.sh && nvm use && lsof -ti:5263 -ti:5400 | xargs kill -9 2>/dev/null; npm run build:website && npx playwright test --config packages/playground/website/playwright/playwright.config.ts shutdown-loopback-prefetch --project=chromium
```

---------

Co-authored-by: Brandon Payton <[email protected]>

* Skip running irrelevant actions on forks (#3473)

## Motivation for the change, related issues

While looking at my actions usage on my personal github account, I
noticed that Playground is using up a lot of actions resources on my
fork, dd32/wordpress-playground.

Upon looking into it, i saw that a bunch of jobs are running that
doesn't appear to be needed.

## Implementation details

- Add github.repository == 'WordPress/wordpress-playground' guard to
publish and refresh workflows so they no longer attempt to run on forks.
- Add a guard to assert a deployment-related secret exists before
running each deployment workflow. That way, even scheduled workflows
abort early by default, until a repository (fork or not) opts in by
defining the secret.

## Testing Instructions (or ideally a Blueprint)

Unfortunately, GH workflow updates are difficult to test. Let's merge
and keep an eye on things afterward.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Adam Zieliński <[email protected]>
Co-authored-by: Brandon Payton <[email protected]>

* Fix broken deploy workflow guards (#3479)

## Summary

Fixes deploy workflows broken by #3473 (by the part I added). That PR
added `secrets.*` checks to job-level `if` conditions, but GitHub
Actions [doesn't make secrets available in that
context](https://docs.github.com/en/actions/writing-workflows/choosing-what-your-workflow-does/accessing-contextual-information-about-workflow-runs#contexts).
This led to an error executing the workflow.

This PR moves the secrets check into a dedicated `check_preconditions`
job that inspects secrets at step level (where they *are* available) and
exposes the result as a job output. The `build_and_deploy` job then
gates on that output.

## Affected workflows

| Workflow | Secret checked |
|---|---|
| `deploy-website.yml` | `DEPLOY_WEBSITE_TARGET_HOST` |
| `deploy-cors-proxy.yml` | `DEPLOY_CORS_PROXY_TARGET_HOST` |
| `deploy-my-wordpress-net.yml` | `DEPLOY_MY_WORDPRESS_NET_HOST` |

## Test plan

- [x] Temporarily allow the CORS proxy deployment to run on this branch
and confirm it runs successfully.
- [ ] Merge to trunk and verify the deploy workflows trigger as expected
- [ ] Confirm the workflows still skip on forks that haven't configured
deploy secrets

* [i18n] Add Bengali translation for blueprints documentation (#3462)

## Motivation for the change, related issues

Continuing Bengali translation efforts for WordPress Playground
documentation. This PR adds Bengali translations for three core
blueprint documentation pages.

## Implementation details

Translated the following files to Bengali:
- `01-index.md` - Getting started with Blueprints (overview and
introduction)
- `02-using-blueprints.md` - How to use Blueprints (URL fragment, query
parameter, bundles, JavaScript API)
- `03-data-format.md` - Blueprint data format (JSON schema, landing
page, preferred versions, features, steps)

All translations:
- Maintain the original structure and formatting
- Preserve all code examples, links, and technical terms
- Use accurate Bengali terminology for technical concepts
- Keep all interactive elements and import statements functional

## Testing Instructions (or ideally a Blueprint)

1. Navigate to the Bengali documentation site when deployed
2. Verify the translated pages render correctly
3. Check that all links and code examples work as expected
4. Confirm Bengali text displays properly with correct Unicode
characters
5. Test interactive Blueprint examples to ensure they function correctly

---------

Co-authored-by: Copilot <[email protected]>

* [Docs] Add `overlay` parameter to Playground URL options (#3457)

This pull request adds new documentation for a UI overlay feature in the
Playground's query API. The main change is an update to the
documentation to describe the new `overlay` URL parameter, which allows
users to open specific overlays (like the Blueprint Gallery)
automatically when the Playground loads.

New feature documentation:

* Added a description of the `overlay` parameter, which lets users open
a UI overlay (such as the Blueprint Gallery) on page load by specifying
`?overlay=blueprints` in the URL. The overlay parameter is removed from
the URL when the overlay is closed.

introduced on PR #3220

---------

Co-authored-by: Yannick Decat <[email protected]>

* Refresh SQLite integration plugin

* Refresh SQLite integration plugin

* [i18n] Added Gujarati translation for wp-playground agent skill guide (#3333)

## Motivation for the change, related issues 
Part of https://github.com/WordPress/wordpress-playground/issues/2320

## Implementation details 

Added Gujarati Translations For  `agent-skill-wp-playground.md` File.

---------

Co-authored-by: Fellyph Cintra <[email protected]>
Co-authored-by: Shail Mehta <[email protected]>
Co-authored-by: Copilot <[email protected]>

* v3.1.19

* chore: update changelog

* [php-wasm-node-polyfills] Remove @php-wasm/node-polyfills package (#3476)

## Motivation for the change, related issues

`@php-wasm/node-polyfills` provides polyfills for `File`,
`Blob.prototype.{arrayBuffer, text, stream}`, BYOB streams, and
`CustomEvent`. All of these are native globals since Node.js 19-20, and
this project requires Node.js >= 20.10.0. Every
polyfill is guarded by `typeof X === 'undefined'`, so they've been
no-ops on our minimum supported Node version for a while.

Node 18 went EOL in April 2025, and downstream dependents that might
need these polyfills all pin older versions explicitly.

## Implementation details

- Removed the `@php-wasm/node-polyfills` side-effect import from:
    - `@php-wasm/logger`
    - `@php-wasm/node`
    - `@php-wasm/universal`
    - `@php-wasm/progress`
    - `@php-wasm/stream-compression`
    - `@wp-playground/blueprints`
- Removed `vitest-setup-files` that only existed to import the polyfills
in `stream-compression` and `blueprints`
- Removed the path alias from `tsconfig.base.json`
- Deleted the `@php-wasm/node-polyfills` package entirely

## Testing Instructions (or ideally a Blueprint)

CI

---------

Co-authored-by: Ashish Kumar <[email protected]>

* Refresh SQLite integration plugin

* [Docs] Removing docs fragments (#3470)

This pull request removes the fragments to ensure compatibility with the
.org script handbook.

* Removed the use of fragment imports like `APIList`, `ThisIsQueryApi`,
`PlaygroundWpNetWarning`, and `JSApiShortExample` from documentation
files, replacing them with inlined content for API lists, caution
messages, and code examples. This streamlines the documentation and
reduces indirection.
[[1]](diffhunk://#diff-d3d4e59bae8ffd4aa654e729787f0c72bfa2ee0705782897f3e4ef5c7d63382cL25-R46)
[[2]](diffhunk://#diff-f03be93b146dbb91e11ad31990acdcbf5835924e196ddfee4176c60cbaf05525L6-L7)
[[3]](diffhunk://#diff-f03be93b146dbb91e11ad31990acdcbf5835924e196ddfee4176c60cbaf05525L26-R24)
[[4]](diffhunk://#diff-f03be93b146dbb91e11ad31990acdcbf5835924e196ddfee4176c60cbaf05525L100-R111)
[[5]](diffhunk://#diff-f03be93b146dbb91e11ad31990acdcbf5835924e196ddfee4176c60cbaf05525L135-R158)
[[6]](diffhunk://#diff-0095dd64399cc6f8a1eea50cc143d22a6758996046f283d03823f3c0dd636bcdL26-R39)
[[7]](diffhunk://#diff-4126a19306fbb861b01bf99c43bf92ceaca6c9f87599717c49e72a8d47e9069cL63-R68)
[[8]](diffhunk://#diff-d98122d016ecc08792994ee1e81bd0726e7f6e8f59cdec48eefa0f64287bdb14L7-L8)
[[9]](diffhunk://#diff-d98122d016ecc08792994ee1e81bd0726e7f6e8f59cdec48eefa0f64287bdb14L57-R55)
[[10]](diffhunk://#diff-d98122d016ecc08792994ee1e81bd0726e7f6e8f59cdec48eefa0f64287bdb14L105-R103)
[[11]](diffhunk://#diff-0c38b392b1b4edd5631d0ff2e78b668ea5b6a16071eebc15b38486743dc14113L158-L159)
[[12]](diffhunk://#diff-0c38b392b1b4edd5631d0ff2e78b668ea5b6a16071eebc15b38486743dc14113L169-R170)
[[13]](diffhunk://#diff-db80de05fde7ebd91ce80973483c68eefbbacd27aed213cc05be44da001a1958L7-L8)
[[14]](diffhunk://#diff-db80de05fde7ebd91ce80973483c68eefbbacd27aed213cc05be44da001a1958L121-R119)
[[15]](diffhunk://#diff-db80de05fde7ebd91ce80973483c68eefbbacd27aed213cc05be44da001a1958L229-R227)
[[16]](diffhunk://#diff-e30c1b5485560dcd5624ca4693acfc79659c1b20aacb1534b2dc82cf8392ee00L45-R51)
[[17]](diffhunk://#diff-e30c1b5485560dcd5624ca4693acfc79659c1b20aacb1534b2dc82cf8392ee00L61-R67)
[[18]](diffhunk://#diff-e30c1b5485560dcd5624ca4693acfc79659c1b20aacb1534b2dc82cf8392ee00L77-R87)
[[19]](diffhunk://#diff-1228d0b32c52ed9a4723aa02249c8525cd005dde4041aed5f9411f5f02f3d88dL142-R154)
[[20]](diffhunk://#diff-0203c8c4ecea6cdefb301778e5bfd8d5e45a65deba51ef6f37037feb008e53fbL7-L8)
[[21]](diffhunk://#diff-0203c8c4ecea6cdefb301778e5bfd8d5e45a65deba51ef6f37037feb008e53fbL121-R119)

**Content clarity and accessibility:**
* API lists are now directly included in the documentation, making it
easier for users to see and choose between the available APIs (Query
API, Blueprints API, JavaScript API) without needing to follow fragment
imports.
[[1]](diffhunk://#diff-4f21997b786e065ab776603867afe7d7e30fe7f098684f664bfbd41cb9951914L1-L3)
[[2]](diffhunk://#diff-d3d4e59bae8ffd4aa654e729787f0c72bfa2ee0705782897f3e4ef5c7d63382cL25-R46)
[[3]](diffhunk://#diff-4126a19306fbb861b01bf99c43bf92ceaca6c9f87599717c49e72a8d47e9069cL63-R68)
[[4]](diffhunk://#diff-0c38b392b1b4edd5631d0ff2e78b668ea5b6a16071eebc15b38486743dc14113L169-R170)
[[5]](diffhunk://#diff-1228d0b32c52ed9a4723aa02249c8525cd005dde4041aed5f9411f5f02f3d88dL142-R154)
* Caution notes about the reliability of the demo site at
`playground.wordpress.net` are now directly embedded in the relevant
docs, improving visibility for users.
[[1]](diffhunk://#diff-fd3fdfc31dbffeb5771bb04a8fafbf409b677a26b2afa59bcea853b7e679e448L1-L5)
[[2]](diffhunk://#diff-d3d4e59bae8ffd4aa654e729787f0c72bfa2ee0705782897f3e4ef5c7d63382cL25-R46)
[[3]](diffhunk://#diff-f03be93b146dbb91e11ad31990acdcbf5835924e196ddfee4176c60cbaf05525L135-R158)
[[4]](diffhunk://#diff-e30c1b5485560dcd5624ca4693acfc79659c1b20aacb1534b2dc82cf8392ee00L45-R51)
* Short JavaScript API usage examples are now inlined, making it easier
for readers to quickly see how to use the API without jumping between
files.
[[1]](diffhunk://#diff-d9389af439381e228e21d33b4981963b202af4294f43e0e8f1c3fcc95b660f57L1-L17)
[[2]](diffhunk://#diff-f03be93b146dbb91e11ad31990acdcbf5835924e196ddfee4176c60cbaf05525L100-R111)
[[3]](diffhunk://#diff-0095dd64399cc6f8a1eea50cc143d22a6758996046f283d03823f3c0dd636bcdL26-R39)

**Translation and localization:**
* All of the above improvements are also applied to the Bengali (`bn`)
and Spanish (`es`) documentation, ensuring consistency across languages.
[[1]](diffhunk://#diff-0c38b392b1b4edd5631d0ff2e78b668ea5b6a16071eebc15b38486743dc14113L158-L159)
[[2]](diffhunk://#diff-0c38b392b1b4edd5631d0ff2e78b668ea5b6a16071eebc15b38486743dc14113L169-R170)
[[3]](diffhunk://#diff-db80de05fde7ebd91ce80973483c68eefbbacd27aed213cc05be44da001a1958L7-L8)
[[4]](diffhunk://#diff-db80de05fde7ebd91ce80973483c68eefbbacd27aed213cc05be44da001a1958L121-R119)
[[5]](diffhunk://#diff-db80de05fde7ebd91ce80973483c68eefbbacd27aed213cc05be44da001a1958L229-R227)
[[6]](diffhunk://#diff-e30c1b5485560dcd5624ca4693acfc79659c1b20aacb1534b2dc82cf8392ee00L45-R51)
[[7]](diffhunk://#diff-e30c1b5485560dcd5624ca4693acfc79659c1b20aacb1534b2dc82cf8392ee00L61-R67)
[[8]](diffhunk://#diff-e30c1b5485560dcd5624ca4693acfc79659c1b20aacb1534b2dc82cf8392ee00L77-R87)
[[9]](diffhunk://#diff-1228d0b32c52ed9a4723aa02249c8525cd005dde4041aed5f9411f5f02f3d88dL142-R154)
[[10]](diffhunk://#diff-0203c8c4ecea6cdefb301778e5bfd8d5e45a65deba51ef6f37037feb008e53fbL7-L8)
[[11]](diffhunk://#diff-0203c8c4ecea6cdefb301778e5bfd8d5e45a65deba51ef6f37037feb008e53fbL121-R119)

**General cleanup:**
* Removed outdated or redundant warning and work-in-progress notices
from documentation fragments.
[[1]](diffhunk://#diff-fd3fdfc31dbffeb5771bb04a8fafbf409b677a26b2afa59bcea853b7e679e448L1-L5)
[[2]](diffhunk://#diff-e4185c1e4b8c960014bfb42384a61266d9e1e9d4b2e6f04ec1aa225a2f3d327fL1-L4)

These changes make the documentation more self-contained, easier to
maintain, and more user-friendly for both English and translated
versions.

**Steps on how to test:**

1. Pull the branch `removing-docs-fragments`
2. Run the docs locally with the command `npm run dev:docs`
3. Navigate to one page with the old fragments:
  - `/developers/build-your-first-app`
  - `/quick-start-guide`
  -  `/`

---------

Co-authored-by: Copilot <[email protected]>

* Restore the missing mounts.spec.ts (#2478)

Co-authored: @ashfame (modifying the PR description below)

## Motivation for the change, related issues

Restores the mounts.spec.ts test file that was mistakenly removed in
https://github.com/WordPress/wordpress-playground/commit/dea8e0b

Since then, mounts.ts has changed significantly which is why tests were
failing and needed to be adapted.

## Testing Instructions (or ideally a Blueprint)

Confirm the CI checks pass.

---------

Co-authored-by: Bero <[email protected]>
Co-authored-by: Ashish Kumar <[email protected]>
Co-authored-by: ashfame <[email protected]>

* [i18n] Add Bengali translation for blueprints tutorial pages (#3461)

## Motivation for the change, related issues

Continuing Bengali translation efforts for WordPress Playground
documentation. This PR adds Bengali translations for two important
blueprint tutorial pages.

## Implementation details

Translated the following files to Bengali:
- `02-how-to-load-run-blueprints.md` - Explains how to load and run
blueprints using URL fragments, Base64 encoding, and blueprint-url
parameter
- `03-build-your-first-blueprint.md` - Step-by-step tutorial for
building a first blueprint including installing themes, plugins, and
importing content

All translations:
- Maintain the original structure and formatting
- Preserve all code examples, links, and technical terms
- Use accurate Bengali terminology for technical concepts
- Keep all interactive elements (Run Blueprint buttons) functional

## Testing Instructions (or ideally a Blueprint)

1. Navigate to the Bengali documentation site when deployed
2. Verify the translated pages render correctly
3. Check that all links and code examples work as expected
4. Confirm Bengali text displays properly with correct Unicode
characters

---------

Co-authored-by: Copilot Autofix powered by AI <[email protected]>
Co-authored-by: Fellyph Cintra <[email protected]>
Co-authored-by: Noruzzaman <[email protected]>

* Fix Safari failing to make cross-origin HTTP requests (#3440)

## Motivation for the change, related issues
**Problem:**
In Safari, WordPress beta tester plugin fails to offer the correct
updates that are available after updating the channels to Beta/RCs in
its settings. This happen because, the request to api.wordpress.org
doesn't go through.

**Root cause:**
Safari does not support `ReadableStream` as a `fetch()` request body,
throwing `"NotSupportedError: ReadableStream uploading is not
supported"`. `fetchWithCorsProxy` used `ReadableStream.tee()` to
duplicate the body for the direct-fetch + CORS-proxy-fallback pattern,
which broke all POST requests (e.g. wp_version_check to
api.wordpress.org) in Safari.

## Implementation details
For browsers that support it, keep the original streaming `teeRequest()`
direct-fetch + CORS-proxy-fallback flow. For Safari (no `ReadableStream`
upload support), buffer the request body into an `ArrayBuffer` so it can
be reused for both the direct fetch attempt and the proxy retry.

The issue is confirmed to fix with this PR, but the issue only surfaces
locally when ran like the following:
```
npx nx build playground-website
cd dist/packages/playground/wasm-wordpress-net
php -S 127.0.0.1:7777 -t .
```
The issue doesn't surface when running `npm run dev`, not sure why.

## Testing Instructions (or ideally a Blueprint)

Checkout this branch locally & run with:

```
npx nx build playground-website
cd dist/packages/playground/wasm-wordpress-net
php -S 127.0.0.1:7777 -t .
```

- Open `http://localhost:7777/?plugin=wordpress-beta-tester` in Safari
browser.
- Go to Dashboard > Updates, only Nightly is offered.
- Go to Tools > Beta Testing
- Select `Bleeding-edge` and then `Beta/RC`, save.
- Now go to Dashboard > Updates, `7.0-RC2` will be offered for upgrade.

---------

Co-authored-by: Copilot <[email protected]>

* [Docs] Updating asyncify page (#3459)

This pull request updates and expands the documentation for Asyncify and
JSPI (JavaScript Promise Integration) in the WordPress Playground
project. It revises the English documentation for clarity and technical
accuracy, and adds comprehensive translations in Spanish and French,
ensuring consistency across all supported languages. The documentation
now also details recent binary size optimizations and current platform
support.

**Documentation improvements and updates:**

* The English documentation in `07-wasm-asyncify.md` has been updated to
clarify the transition from Asyncify to JSPI, explain current support in
Node.js and browsers, and describe the impact of using Emscripten's
`MAIN_MODULE=2` flag for binary size reduction.

**Internationalization:**

* Portuguese (`pt-BR`), Spanish (`es`), and French (`fr`) translations
of the Asyncify and JSPI documentation have been added, closely
mirroring the revised English content, including technical explanations,
troubleshooting steps, and optimization details.
[[1]](diffhunk://#diff-7423f6d9a36b19eab981b6e605d10357bf3b202cb4445783b103ad714578ac23R1-R149)
[[2]](diffhunk://#diff-7f840ce415f9e635e3c238a74911411fa78e5bc588870a45f7dd79987878feb3R1-R149)

**steps how to test**

1. pull the branch `updating-asyncify-page`
2. Install the dependencies
3. run the command `npm run dev:docs`
4. Check the updates at the address
`/developers/architecture/wasm-asyncify`

---------

Co-authored-by: Yannick Decat <[email protected]>

* [Docs] Updating blueprint bundles page (#3465)

This pull request updates and significantly expands the documentation
for Blueprint bundles in both English and Brazilian Portuguese. The
changes clarify bundle formats, add step-by-step usage instructions, and
introduce new sections on ZIP structure flexibility, troubleshooting,
and translation improvements.

Key documentation improvements:

**Expanded and clarified documentation (English, French, Spanish, and
Portuguese):**
* Added detailed explanations of what Blueprint bundles are, their
supported formats, and how to use them with both the website and CLI,
including practical examples and command-line usage.
[[1]](diffhunk://#diff-a4ea9d914246aaa818b46e1d4c44cfb75857563b39735e004618b3a39a9b6ba9L4-R122)
[[2]](diffhunk://#diff-a4ea9d914246aaa818b46e1d4c44cfb75857563b39735e004618b3a39a9b6ba9L81-R180)
* Introduced a new section on ZIP file structure flexibility, explaining
that `blueprint.json` can be located either at the root or one directory
deep, and that macOS's `__MACOSX` directory is ignored. Provided
illustrative examples of valid ZIP layouts and clarified error behavior
for ambiguous bundles.
[[1]](diffhunk://#diff-70d09fd00bd332186a91f1cbf9032b0c9038da26cd54ea4bbe2fdd79c078e989R143-R176)
[[2]](diffhunk://#diff-a4ea9d914246aaa818b46e1d4c44cfb75857563b39735e004618b3a39a9b6ba9L227-L272)
* Added troubleshooting guidance and updated instructions to reflect the
new ZIP structure flexibility and best practices for bundle creation and
usage.
[[1]](diffhunk://#diff-70d09fd00bd332186a91f1cbf9032b0c9038da26cd54ea4bbe2fdd79c078e989R143-R176)
[[2]](diffhunk://#diff-a4ea9d914246aaa818b46e1d4c44cfb75857563b39735e004618b3a39a9b6ba9L227-L272)

---------

Co-authored-by: Copilot <[email protected]>
Co-authored-by: Yannick Decat <[email protected]>

* Refresh SQLite integration plugin

* [Github Actions] Fix GitHub release publishing wrong version (#3488)

## Motivation for the change, related issues

The `Publish GitHub Release` workflow used `workflow_run.head_sha` to
checkout the codebase, but that SHA is captured when the `Release NPM
packages` workflow starts, before `lerna publish` bumps the version.
This caused the GitHub release to always be created for the previous
version instead of the current one.

## Implementation details

- `publish-github-release.yml` : Changed the checkout ref from
`head_sha` (pre-bump commit) to `head_branch` (tip of trunk, which
includes both the version bump and changelog commits).
- `publish-npm-packages.yml` : Added a concurrency group to prevent
overlapping NPM releases, which ensures trunk doesn't move to a newer
version while the GitHub release workflow is running.

## Testing Instructions after merge

1. Trigger the "Release NPM packages" workflow manually.
2. Verify the "Publish GitHub Release" workflow creates a release
matching the version in `lerna.json` after the bump.
3. Confirm the changelog content in the GitHub release matches the
correct version entry.

* [ xdebug ] Add Xdebug use case examples (#3075)

## Motivation for the change, related issues

To help developers familiarize themselves with the Xdebug dynamic
extension, several use case examples have been implemented.

## Implementation details

Added Xdebug use cases for : 

```
- PHP.wasm CLI in VSCode
- PHP.wasm Node in VSCode
- Playground CLI in VSCode
```
```
- PHP.wasm CLI in PhpStorm
- PHP.wasm Node in PhpStorm
- Playground CLI in PhpStorm
```
```
- PHP.wasm CLI in Chrome Devtools
- PHP.wasm Node in Chrome Devtools
- Playground CLI in Chrome Devtools
```

## Testing Instructions (or ideally a Blueprint)

- Clone this repository
- cd `examples/xdebug/ide` or `examples/xdebug/chrome-devtools` 
- explore the different use cases and follow the steps in the dedicated
`README.md` files

---------

Co-authored-by: Ashish Kumar <[email protected]>
Co-authored-by: Copilot <[email protected]>
Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: ashfame <[email protected]>

* Refresh SQLite integration plugin

* [PHP] Fix gzip crash – add missing zlib/curl functions to ASYNCIFY_ONLY (#3491)

## Summary

When PHP calls `file_get_contents()` or any other network I/O inside a
`CURLOPT_WRITEFUNCTION` callback while curl is auto-decompressing a gzip
response, Asyncify unwinds the WASM call stack through zlib's
`inflate()` pipeline. Six functions on that path were missing from the
`ASYNCIFY_ONLY` list, so Asyncify couldn't save and restore their locals
during unwind/rewind. The corrupted state made `updatewindow()` compute
an out-of-bounds pointer on the next inflate call, triggering
`RuntimeError: unreachable`.

The missing functions:

- `inflate`, `inflate_fast`, `inflate_table` — zlib internals
- `inflate_stream`, `gzip_unencode_write` — curl's zlib wrappers
- `Curl_httpchunk_read` — curl's chunked HTTP decoder

Adding them to `ASYNCIFY_ONLY` lets Asyncify properly checkpoint and
resume their stack frames, so zlib's window pointer stays coherent
across the unwind/rewind cycle.

The regression test reproduces the crash exactly: it fetches a
gzip-compressed chunked HTTP response (50 SQL chunks with
`Z_SYNC_FLUSH`) while calling `file_get_contents()` inside the
`WRITEFUNCTION` callback. Before this fix: 10/10 crash. After: 10/10
pass.

Rebuilt PHP 8.4 node/asyncify with the fix. Remaining PHP versions need
a rebuild before merge.

Relates to #2957.

## Test plan

- [x] New regression test reproduces the crash reliably on the old build
(10/10)
- [x] All 10 runs pass with the fix applied
- [x] PHP 8.4 node/asyncify rebuilt — all networking tests pass (curl,
HTTPS, etc.)
- [ ] Rebuild remaining PHP versions before merge

* v3.1.20

* chore: update changelog

* [PHP] Add PHP 5.2 WebAssembly builds and runtime support (#3501)

## Summary

- Introduce `LegacyPHPVersions` / `AllPHPVersions` type system and
register PHP 5.2.17 in the version registry
- Add Dockerfile and C-source changes for PHP 5.2 compilation: autoconf
2.13, pre-generated parser/scanner downloads, flex wrapper, polyfill
extension compat (`ZEND_FE_END`/`PHP_FE_END`), yytext
duplicate-definition handling, mysqlnd gating, CLI getopt linking,
`php_cli_process_title` version guard
- Commit pre-built PHP 5.2 WASM binaries for all four targets (web/node
× JSPI/Asyncify)
- Wire PHP 5.2 into the web worker and CLI: legacy PHP ini pre-creation
(disables `ini_get_all`, OPcache), single-instance mode for PROXYFS,
correct SQLite plugin selection, extension gating, PHP 5.2-compatible
mu-plugin stub
- Support downloading non-minified legacy WordPress versions from
wordpress.org (1.0–6.2)

## Test plan

- [x] `npx nx recompile-php:jspi php-wasm-web -- --PHP_VERSION=5.2`
succeeds
- [x] `npx nx recompile-php:asyncify php-wasm-web -- --PHP_VERSION=5.2`
succeeds
- [x] `npx nx recompile-php:jspi php-wasm-node -- --PHP_VERSION=5.2`
succeeds
- [x] `npx nx recompile-php:asyncify php-wasm-node -- --PHP_VERSION=5.2`
succeeds
- [x] `npx nx typecheck playground-website` passes
- [x] `npx nx run-many -t lint
--projects=php-wasm-web,php-wasm-node,php-wasm-universal,playground-website`
passes

* Refresh SQLite integration plugin

* [Docs] Update trusted publisher setup instructions (#3505)

Clarify the instructions for adding a trusted publisher.

* [PHP] Align @php-wasm/{web,node}-5-2 with workspace and add READMEs (#3506)

## Summary

- Bumps the newly-introduced `@php-wasm/web-5-2` and
`@php-wasm/node-5-2` packages from `3.1.19` → `3.1.20` to match the
current workspace version (`lerna.json`). Without this they would have
been published below their own `@php-wasm/[email protected]` dependency
and skipped by the next `lerna publish patch`.
- Adds `README.md` for both packages, mirroring the 8-5 style. The blurb
notes that 5.2 bundles no extensions (intl/Xdebug/Redis/Memcached
getters all throw), so the npmjs.com page isn't blank and consumers
aren't surprised.

Follow-up to #3501. Both packages are already live on npm at `3.1.20` —
this PR brings the source tree in sync so the weekly `lerna publish` CI
picks up cleanly from here.

## Test plan

- [x] `npm view @php-wasm/web-5-2 version` → `3.1.20`
- [x] `npm view @php-wasm/node-5-2 version` → `3.1.20`
- [x] `npm access list collaborators @php-wasm/web-5-2` matches
`@php-wasm/web-8-5`
- [x] Trusted Publisher configured on npmjs.com for both packages (repo
`WordPress/wordpress-playground`, workflow `publish-npm-packages.yml`,
environment `npm`)

* Refresh SQLite integration plugin

* Refresh SQLite integration plugin

* Refresh SQLite integration plugin

* [PHP.wasm] Document bundler configuration for the .dat file import in `@php-wasm/web` (#2776)

Added instructions for using `@php-wasm/web` with bundlers like Vite.
The `import dataFilename from './icudt741.dat';` like published in the
package will typically trip up bundlers with their default
configuration.

cc @fellyph @mho22 @brandonpayton

* [i18n] Docs: Add Gujarati translation for intro.md file (#3508)

## Motivation for the change, related issues
Part of https://github.com/WordPress/wordpress-playground/issues/2320

## Implementation details
Added `Gujarati Translations` For `blueprints/intro.md` File.

* [i18n] Added Gujarati Translation for Blueprints 01-index.md File (#3507)

## Motivation for the change, related issues
Part of https://github.com/WordPress/wordpress-playground/issues/2320

## Implementation details
Added `Gujarati Translations` For `blueprints/01-index.md` File.

* [Github actions] Modify the time slot of the `refresh-sqlite-integration` workflow (#3510)

## Motivation for the change, related issues

The `Publish NPM packages` workflow [failed
today](https://github.com/WordPress/wordpress-playground/actions/runs/24659493612).
And based on the stack trace : pushing the branch `trunk` was rejected
because the remote is ahead. The only file modification on `trunk` is
`packages/playground/wordpress-builds/src/sqlite-database-integration/sqlite-database-integration-trunk.zip`
because of [this
commit](https://github.com/WordPress/wordpress-playground/commit/4e2c90c900c8771c736469a34d428f64d5a8039a).
Which I suspect made the process of publishing NPM packages fail.

Both workflows triggered at the same time, and the other workflow pushed
that commit to trunk before the publish workflow could push its version
bump commit.

## Implementation details

Modified cron to be`'0 10 * * *'` instead of same time slot as
`publish-npm-packages`: '0 9 * * *'

## Testing Instructions

`Publish NPM packages` workflow completion next monday.

* [Website] Add error notices and delete confirmation modal to site management (#3454)

## Motivation for the change, related issues

Follow-up to PR feedback on #3401. The `PlaygroundSitesAPI` methods
throw descriptive errors for MCP consumers, but UI components were
silently swallowing them. Additionally, site deletion used
`window.confirm`, which is inconsistent with the rest of the modal-based
UI and blocks the main thread.

## Implementation details

### Error notices in modals
- **Rename modal**: catches errors from `sitesAPI.rename()` and displays
them in a `<Notice>` component.
- **Save modal**: surfaces the actual error message (instead of a
generic string) and moves the error notice above the action buttons for
visibility.
- **Delete modal** (new): catches errors from `sitesAPI.delete()` and
displays them inline.

### Delete confirmation modal
- Replaces `window.confirm` with a new `DeleteSiteModal` component,
consistent with the existing rename/save modal pattern.
- Adds `DELETE_SITE` modal slug and `siteSlugToDelete` UI state to the
Redux store.
- Both the site info panel and saved playgrounds overlay now dispatch
the modal instead of calling `window.confirm`.

### API documentation
- Adds JSDoc comments with `@param`, `@returns`, and `@throws` tags to
every method on the `PlaygroundSitesAPI` interface.

### Minor cleanups
- Inlines `getActiveSiteOrThrow` — each call site now checks and throws
with a context-specific message (`"No active site selected"`).
- `persistTemporarySite` now lets `showDirectoryPicker` errors propagate
instead of silently returning, so callers can handle cancellation.
- Removes post-persist `storage === 'none'` safety-net checks that are
no longer needed.

## Testing Instructions

## Testing Instructions

1. In `save-site-modal/index.tsx`, add `throw new Error('test')` before
the `persistTemporarySite` call in `handleSubmit`
2. Open the Playground website and try saving a temporary site — confirm
the error appears as a notice inside the modal
3. Repeat for `rename-site-modal/index.tsx` (before `sitesAPI.rename()`)
and `delete-site-modal/index.tsx` (before `sitesAPI.delete()`)
4. Remove the temporary `throw` statements
5. Save, rename, and delete a site — confirm each operation succeeds and
the modal closes


<img width="1512" height="863" alt="Screenshot 2026-04-01 at 16 39 38"
src="https://github.com/user-attachments/assets/2e45c6a1-618d-478b-aead-d098bc701a00"
/>
<img width="1512" height="862" alt="Screenshot 2026-04-01 at 16 39 50"
src="https://github.com/user-attachments/assets/e6f97182-3632-4eac-852c-5e5ffdc41d63"
/>
<img width="1512" height="862" alt="Screenshot 2026-04-01 at 16 40 44"
src="https://github.com/user-attachments/assets/ce9c889f-176a-43ea-b358-7b1c84905e09"
/>

---------

Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
Co-authored-by: Ashish Kumar <[email protected]>

* [PHP] Fix MEMFS symlink crash during PHP runtime rotation (#3500)

## Motivation for the change, related issues

`copyMEMFSNodes` crashes when it encounters a symlink to a directory
during PHP runtime rotation. This happens because lookupPath defaults to
`{ follow: true }`, which resolves the symlink and treats it as a
regular directory, leading to incorrect copying and crashes.

## Implementation details

In `copyMEMFSNodes`, use lookupPath with `{ follow: false }` to detect
symlinks before following them. When a symlink is found, recreate it via
`symlink()` instead of attempting to copy the target's contents.

## Testing Instructions (or ideally a Blueprint)

CI

* Refresh SQLite integration plugin

* [i18n] Create index.md and add French text (#3167)

## Implementation details
Create the file index.md in the guide folder
Add French text

---------

Co-authored-by: Fellyph Cintra <[email protected]>
Co-authored-by: Yannick Decat <[email protected]>

* [CLI] make --no-auto-mount actually disable auto-detection on start (#3503)

## Motivation for the change, related issues

The documented invocation `wp-playground start --no-auto-mount` fails
with `Unknown arguments: auto-mount, autoMount`, so there is no way to
disable auto-detection on the `start` command.

Root cause: the `start` command declared a literal `no-auto-mount`
boolean option. yargs-parser's built-in boolean-negation (combined with
camel-case-expansion) rewrites `--no-auto-mount` into `{ "auto-mount":
false, autoMount: false }` against an *undefined* `auto-mount` option.
With `strictOptions()` enforced, those phantom keys get rejected as
unknown.

## Implementation details

- Rename the option from `no-auto-mount` (boolean, default `false`) to
`auto-mount` (boolean, default `true`) on the `start` command.
yargs-parser's negation now matches a defined option, so
`--no-auto-mount` parses cleanly as `{ autoMount: false }` — no phantom
keys, no strictOptions rejection. `--auto-mount=false` and
`--noAutoMount` also continue to work.
- Update `expandStartCommandArgs` to gate on `args.autoMount !== false`.
Because yargs stores the boolean in the same `autoMount` property the
`server` command uses for a string path, the boolean form is scrubbed
before any downstream consumer sees it.
- Widen `RunCLIArgs.autoMount` to `string | boolean` (start = boolean
toggle; server & friends = string path). Drop the now-unused
`RunCLIArgs.noAutoMount`. No public consumers of `noAutoMount` were
found inside the monorepo.
- Guard the shared `.check()` path-validation block so the "not a
directory" check only runs for subcommands where `--auto-mount` is a
string. Otherwise the `start` command's boolean `true` would be fed to
`fs.statSync()` and blow up.

Alternative I considered but rejected: disabling `boolean-negation` on
the yargs instance. That would silently change the semantics of every
other `--no-X` flag in the CLI — broader blast radius than a targeted
rename.

## Testing Instructions (or ideally a Blueprint)

Automated:
- `npx nx typecheck playground-cli`
- `npx nx lint playground-cli`
- `npx nx test-playground-cli playground-cli --testFile=mounts.spec.ts`
— confirms mount logic is untouched (20/20 pass).
- `npx nx test-playground-cli playground-cli
--testNamePattern="--no-auto-mount"` — new integration test boots `start
--no-auto-mount` via `parseOptionsAndRunCLI`, asserts the server URL
comes up, and asserts the sample-plugin directory is *not* mounted under
`/wordpress/wp-content/plugins/`. Requires network (WordPress download),
so it runs in CI rather than offline.

Manual:
1. `npx nx build playground-cli`
2. In a scratch dir with a plugin file (`my-plugin.php` with a `Plugin
Name:` header):
- `wp-playground start --no-auto-mount --path=./` — server boots; the
plugin is *not* visible under `/wordpress/wp-content/plugins/`.
- `wp-playground start --path=./` — server boots; the plugin *is*
auto-mounted and activated (default behavior, unchanged).
- `wp-playground start --auto-mount=false --path=./` — equivalent to
`--no-auto-mount`.
- `wp-playground start --noAutoMount --path=./` — the legacy camelCase
workaround still works.
3. `wp-playground start --help` shows `--auto-mount` (with its describe
text noting `--no-auto-mount` disables it) instead of the old
`--no-auto-mount` entry.

---------

Co-authored-by: Ashish Kumar <[email protected]>

* [CLI] Add --workers=<n|auto> flag to configure worker thread count (#3504)

## Motivation for the change, related issues

The Playground CLI hardcodes the number of request-handling worker
threads to 6 (chosen to match HTTP/1.1's per-origin browser connection
limit). That's fine for a single browser, but it caps multi-client
workloads — parallel Playwright contexts, load tests, fanning e2e suites
— at six in-flight requests, which is too few on modern hosts.

The old `--experimental-multi-worker` flag that let users override this
was deprecated and its value is now silently ignored, and the
pre-hardcoding default of `cpus - 1` was dropped along with it. There's
no way to turn the dial back up.

## Implementation details

Adds a new `--workers=<n|auto>` flag:

- A positive integer sets an explicit worker count.
- `auto` uses `max(1, os.cpus().length - 1)` — restores the old
pre-hardcoding default.
- When the flag is absent, the default is `min(6, max(1, cpus - 1))`:
preserves today's 6-worker behavior on machines with ≥7 cores, shrinks
on small hosts so we don't spawn more workers than cores. The
`Math.max(1, ...)` guard covers single-core hosts and restricted
environments where `os.cpus()` returns an empty array.
- Validation happens in yargs `coerce`: rejects 0, negatives,
non-integers, and any non-`auto` string with a clear error message,
routed through the existing `.fail()` handler.

The resolver is a small exported pure function (`resolveWorkerCount`) so
the min/max/cpu logic is unit-testable without spinning up a server.

`--experimental-multi-worker` stays parsed-but-ignored — existing
invocations don't break — but now emits a one-line `logger.warn`
pointing users at `--workers`.

Docs updated in both `packages/playground/cli/README.md` and the
Docusaurus CLI reference page.

### Breaking changes

None. The default worker count is unchanged on typical dev machines (≥7
cores → still 6). On hosts with 1–6 cores the default drops to `cpus -
1`, which is a behavior change on small hosts but strictly safer (we
were previously spawning more workers than cores there).

## Testing Instructions (or ideally a Blueprint)

**Unit tests** (fast, no server spawn):

```bash
npx nx run playground-cli:test-playground-cli --testNamePattern="resolveWorkerCount|worker count"
```

All 14 new tests should pass: default, explicit number, `--workers=1`
(single-worker bootstrap), `--workers=auto`, invalid `0`, invalid `abc`,
deprecation warning for `--experimental-multi-worker`, plus 7 unit tests
stubbing `os.cpus()` to cover the min/max/empty-array edges.

**Manual smoke**:

```bash
# Explicit count — server prints "(12 workers)"
npx nx dev playground-cli server --workers=12

# auto — uses cpus-1
npx nx dev playground-cli server --workers=auto

# default — min(6, cpus-1)
npx nx dev playground-cli server

# Deprecated flag still works, now warns
npx nx dev playground-cli server --experimental-multi-worker=4

# Validation
npx nx dev playground-cli server --workers=0      # exits 1 with clear error
npx nx dev playground-cli server --workers=abc    # exits 1 with clear error
```

**Multi-client verification** (the motivating use case):

```bash
# Start with more than 6 workers
npx nx dev playground-cli server --workers=16 &

# Fire >6 concurrent requests; throughput should scale past the old 6-worker ceiling
seq 1 32 | xargs -P32 -I{} curl -s -o /dev/null -w "%{time_total}\n" http://127.0.0.1:9400/
```

* [PHP] Add `TSRMLS_CC` fallback defines for PHP versions above 7 (#3512)

## Motivation for the change, related issues

[Add PHP 5.2 WebAssembly builds and runtime
support](https://github.com/WordPress/wordpress-playground/pull/3501)
added `TSRMLS_CC` macros to `dns_polyfill` and `post_message_to_js` for
PHP 5.x compatibility, but these macros don't exist in PHP 7+. This
breaks recompilation of PHP.wasm for all PHP versions 7.0 through 8.5.

## Implementation details

Added empty `TSRMLS_CC` fallback defines to `dns_polyfill.c` and
`post_message_to_js.c`

## Testing Instructions (or ideally a Blueprint)

- [x] `nx run php-wasm-node:recompile-php:jspi`
- [x] `nx run php-wasm-node:recompile-php:asyncify`
- [x] `nx run php-wasm-web:recompile-php:jspi`
- [x] `nx run php-wasm-web:recompile-php:asyncify`

* Refresh SQLite integration plugin

* [CLI] Pin file-locking test suite to 3 workers (#3521)

## Motivation for the change, related issues

Follow-up to #3504.

`test-playground-cli (macos-latest)` started failing consistently after
#3504 merged. Every failing run showed the same five timeouts in
`packages/playground/cli/tests/file-locking.spec.ts`:

- `PHP flock() > should grant multiple shared locks on a file`
- `PHP flock() > should release a shared lock when its associated file
descriptor is closed`
- `PHP flock() > should release an exclusive lock when its associated
file descriptor is closed`
- `PHP flock() > should release a shared lock when the owning process
exits`
- `PHP flock() > should release an exclusive lock when the owning
process exits`

### Root cause

The `multi shared locks` test at `file-locking.spec.ts:1090` fires three
concurrent PHP requests with `Promise.all`. Each script busy-waits on a
coordination file that only the *next* script in the chain can write, so
all three must run concurrently.

Before #3504 the CLI hardcoded 6 workers, which was always enough. After
#3504 the default dropped to `min(6, max(1, cpus - 1))`. GitHub's
`macos-latest` runners report 3 CPUs, resolving to **2 workers** — so
PHP3 is permanently queued and the test deadlocks.

### Why four unrelated tests also fail

The `runCLI` server is created once in `beforeAll` and shared across
every test in the `describe`. When the multi-shared test times out, its
two workers are still spinning inside PHP-level `while
(file_get_contents(...) \!== ...)` loops — vitest can abort the JS
`await`, but there's no hook to unblock PHP. The four two-script tests
that follow multi-shared in file order each do
`Promise.all([fetchScript(php1), fetchScript(php2)])`, inherit a pool
with zero free workers, and time out at 60 s each.

Evidence this is the right model:
- The two-script tests that run *before* multi-shared pass
(deny-exclusive-with-shared, deny-shared-with-exclusive). The two-script
tests *after* fail. Failures aren't interleaved among the earlier ones.
- Every failure is a 60 s timeout, never an assertion error. Independent
flock races would surface as `expect(lock_acquired).toBe(false)`-style
failures.
- A 200-run audit of `test-playground-cli (macos-latest)` across all
branches shows no consistent failure pattern before #3504 merged.

## Implementation details

Pass `workers: 3` to `runCLI()` in the suite's `beforeAll`. Three is the
minimum the multi-shared test needs; anything lower re-introduces the
cascade. Explicit integer values bypass the `min(6, cpus-1)` default
clamp (confirmed by the `honors an explicit --workers=3` test at
`run-cli.spec.ts:1886`), so the suite is now robust to host CPU count.

No production code changes. Only the test file is touched.

## Testing Instructions (or ideally a Blueprint)

```bash
# Passes on macOS-like hosts with 3 CPUs:
npx nx test-playground-cli playground-cli --testFile=file-locking.spec.ts

# To reproduce the original failure, temporarily change workers: 3 to workers: 2:
# expect 5 timeouts in the PHP flock() describe.
```

On CI, the `test-playground-cli (macos-latest)` job should turn green.
Ubuntu and Windows runners were already passing (their default worker
count was already ≥3) and should remain so.

## AI disclosure

Per [WordPress AI
Guidelines](https://make.wordpress.org/ai/handbook/ai-guidelines/):

**AI assistance:** Yes
**Tool:** Claude Code (Claude Opus 4.7)
**Used for:** Root-cause analysis of the worker-starvation cascade,
drafting the fix and this PR description. All code was reviewed and
tested by the author.

* [Remote] Re-enable client-side media processing via Document-Isolation-Policy (#3515)

## Motivation for the change, related issues

Fixes #3514.

Playground currently disables Gutenberg's **client-side media
processing** experiment via an `__return_false` filter in the mu-plugin
([0-playground.php#L265-L274](https://github.com/WordPress/wordpress-playground/blob/trunk/packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php#L265-L274)).
That workaround was added in #3312 because, at the time, Gutenberg was
emitting COEP/COOP for the editor and — even when rewritten to
`Document-Isolation-Policy` by the service worker — the editor's inner
iframe was crashing.

[Gutenberg PR #75991](https://github.com/WordPress/gutenberg/pull/75991)
(shipped in 22.6+) changed the isolation strategy: on Chromium 137+
Gutenberg now sends `Document-Isolation-Policy:
isolate-and-credentialless` **directly** on editor screens and no longer
sends COEP/COOP. That removes the root cause of the breakage — but it
also means Playground's existing COEP→DIP rewrite path never fires on
current Gutenberg, so the scope is never added to
`scopesWithCrossOriginIsolation`, `empty.html` doesn't receive DIP, and
parent/child DIP parity breaks inside the block editor canvas (see #3320
for why parity matters).

## Implementation details

Two small, coordinated changes:

1. **`packages/playground/remote/service-worker.ts`** — broaden the
post-response handler (now `applyCrossOriginIsolationHeaders`) so that:
- If a response already carries `Document-Isolation-Policy`, the scope
is added to `scopesWithCrossOriginIsolation` and the response passes
through unchanged. This is the modern path once Gutenberg serves DIP
directly.
- If a response carries COEP/COOP, the existing rewrite path continues
to operate unchanged (covers older Gutenberg, WP core's
`wp_set_up_cross_origin_isolation`, or custom plugins).

The existing `empty.html` branch (which adds DIP to the inner editor
iframe when the scope is tracked) needs no change — it now gets
populated in both cases.

2.
**`packages/playground/remote/src/lib/playground-mu-plugin/0-playground.php`**
— remove `add_filter('wp_client_side_media_processing_enabled',
'__return_false')` and its comment block. The workaround is no longer
necessary.

## Tests

New Playwright spec:
`packages/playground/website/playwright/e2e/client-side-media.spec.ts`

- Chromium-only (skipped in Firefox/Safari — DIP and client-side media
are Chromium-only today).
- Boots Playground with Gutenberg + `gutenberg-media-processing`
experiment on `/wp-admin/post-new.php` and asserts, inside the WP admin
iframe:
  - `window.crossOriginIsolated === true`
  - `typeof SharedArrayBuffer !== 'undefined'`
- `window.__clientSideMediaProcessing === true` (Gutenberg's own
enablement flag)

Editor rendering under DIP is already covered by the existing
`document-isolation-policy.spec.ts` suite, which implicitly verifies
parent/child DIP parity.

## Testing instructions

Manual (Chromium ≥ 137):

1. Run `npm run dev` and open `http://127.0.0.1:5400/website-server/`.
2. Load Guetnberg with this Blueprint (paste into the URL after `#`):

```json
{
  "$schema": "https://playground.wordpress.net/blueprint-schema.json",
  "landingPage": "/wp-admin/post-new.php",
  "plugins": ["gutenberg"],
  "login": true,
  "steps": [
    {
      "step": "runPHP",
      "code": "<?php require '/wordpress/wp-load.php'; update_option('gutenberg-experiments', array('gutenberg-media-processing' => true));"
    }
  ]
}
```

3. The post editor loads without crashing. 
4.  In the WP iframe's DevTools console:
   - `crossOriginIsolated` → `true`
   - `typeof SharedArrayBuffer` → `"function"`
   - `window.__clientSideMediaProcessing` → `true`
5. The admin document response carries `Document-Isolation-Policy:
isolate-and-credentialless` and no COEP/COOP.
6. Upload an image — it is processed client-side (wasm-vips loads; no
server-side sub-size generation request).
7. Upload a small AVIF image (sample attached below) - it works (before
this PR it fails)
8. Non-Chromium (Firefox/Safari): editor still opens; client-side media
is simply not engaged (unchanged behavior).
9. Embedded Playground on a 3rd-party page with no COEP/COOP still loads
(DIP is per-document, so this should hold).
10. Embedding 3p elements on the page, eg. a YouTube embed, still work
11. Iframe using elements such as the classic block work as expected

Automated:

```
npx nx e2e playground-website --grep="Document-Isolation-Policy|client-side media|crossOriginIsolated"
```

## Notes on backwards compatibility

- No user-facing breaking changes.
- The COEP→DIP rewrite path is preserved, so older WordPress/Gutenberg
versions, or plugins that set COEP/COOP themselves, keep working as they
do today.
- Non-Chromium browsers: no regression — DIP is unsupported there,
Playground's feature detection returns `false`, headers pass through,
and Gutenberg's JS feature detection falls back the same as before.

* Refresh SQLite integration plugin

* Add explicit OPFS flush API (#3517)

## Motivation for the change

Embedders that use OPFS-backed Playground instances need a reliable way
to wait until recent filesystem writes are persisted before allowing a
page refresh or teardown.

Today, OPFS persistence is handled internally by the directory-handle
journal and background flushes, but there is no public API for an
embedder to explicitly await that flush. This forces downstream
integrations to approximate persistence by remounting or syncing
selected directories, which is slower and can be incorrect when a save
touches multiple areas of the WordPress filesystem.

This PR adds a narrow mount-level API:

```ts
await playground.flushOpfs("/wordpress");
```

The API lets embedders wait for the existing OPFS journal to drain
without exposing path-level persistence semantics or requiring a full
site-tree resync.

## Implementation details

- Adds a `DirectoryHandleMount` controller for OPFS directory-handle
mounts with:
  - `flush(): Promise<void>`
  - `unmount(): Promise<void>`
- Routes existing automatic OPFS journal flushes through the same
serialized flush path used by explicit flushes.
- Exposes `flushOpfs(mountpoint)` through the remote/client API.
- Tracks active OPFS mount controllers by mountpoint in
`PlaygroundWorkerEndpoint`.
- Registers boot-time OPFS mounts created during the Blueprints v1 boot
path, so the primary `/wordpress` mount can be flushed explicitly.
- Makes `unmountOpfs(mountpoint)` await `flushOpfs(mountpoint)` before
unmounting.
- Cleans up OPFS and PHP mount tracking even when flush/unmount paths
fail, while preserving the original failure for callers.
- Throws a clear error when `flushOpfs()` or `unmountOpfs()` is called
for a mountpoint that is not an active OPFS mount.

The API is intentionally mount-level rather than path-scoped. A single
WordPress save may update the SQLite database, uploads, themes, plugins,
or other files, and exposing path-scoped flushes would make it too easy
for embedders to claim refresh-safe persistence while missing related
writes.

## Testing Instructions (or ideally a Blueprint)

Automated checks run locally:

```bash
npx nx test php-wasm-web
npx nx test playground-remote
npx nx test php-wasm-universal
npx nx typecheck php-wasm-web
npx nx typecheck playground-remote
npx nx typecheck php-wasm-universal
npx nx lint php-wasm-web
npx nx lint playground-remote
npx nx lint php-wasm-universal
npx nx build php-wasm-universal
npx nx build php-wasm-web
CORS_PROXY_URL="" npx nx build playground-remote
```

Added/updated test coverage includes:

- Pending journaled file changes are written by explicit `flush()`.
- Concurrent flush calls reuse/serialize the in-flight flush.
- Writes that arrive while a flush is running are included before the
explicit flush resolves.
- Existing automatic `filesystem.write` and `request.end` flush triggers
still flush pending writes.
- Background flush failures are logged without creating unhandled
promise rejections.
- Failed explicit flushes can be retried.
- Never-settling journal flushes fail with a clear mountpoint-specific
error instead of hanging forever.
- `unmount()` flushes pending writes before removing listeners and still
removes listeners when flush fails.
- PHP mount tracking is cleared even when the underlying unmount
callback fails.
- `flushOpfs("/wordpress")` calls the active OPFS mount controller.
- Missing mountpoints throw a clear error.
- `hasOpfsMount()` reflects active OPFS mount controllers.
- `unmountOpfs("/wordpress")` flushes before unmounting and removes
mount tracking on both success and failure.
- Blueprints v1 boot-time OPFS mounts are routed through the
registration path used by `flushOpfs()`.

---------

Co-authored-by: ashfame <[email protected]>
Co-authored-by: Ashish Kumar <[email protected]>

* Refresh SQLite integration plugin

* [docs] Replacing info component by compatible handbook element (#3496)

## Motivation for the change, related issues

This pull request replaces the `:::info` component with a div that is
compatible with the Handbook documentation. When the page is loaded from
the handbook, the style will be applied. This PR includes a fallback on
the current docs with the `custom.css` file.

## Implementation details

The component from docusaurus `:::info` was replaced by `<div
class="callout callout-info">`, this first pull request will replace the
component only for the English version.

Previous design:
<img width="919" height="215" alt="Screenshot 2026-04-15 at 12 22 18"
src="https://github.com/user-attachments/assets/f23ff1d5-cae4-4389-bbd6-0fd354f8c817"
/>

New design:
<img width="888" height="315" alt="Screenshot 2026-04-15 at 12 03 58"
src="https://github.com/user-attachments/assets/985c4120-5ad9-4138-ac88-0592c4efbbb7"
/>

## Testing Instructions (or ideally a Blueprint)

1. Pull the branch `replacing-info-block-docs`
2. Install the dependencies `npm install`
3. Run the docs locally `npm run dev:docs`
4. Check the component info on the home page

---------

Co-authored-by: Ashish Kumar <[email protected]>

* [Asyncify] Fix `fix-asyncify` command (#3509)

## Motivation for the change, related issues

Related to [this
issue](https://github.com/WordPress/wordpress-playground/issues/3472)

The `npm run fix-asyncify` command was broken because the
`test-php-asyncify-all` NX target was removed from
`packages/php-wasm/node/project.json` in a previous pull request. The
`rebuild-while-asyncify-functions-missing.mjs` script depends on this
target to run all asyncify test groups, so without it, the
`fix-asyncify` workflow fails.

## Implementation details

Re-added the `test-php-asyncify-all` target to
`packages/php-wasm/node/project.json`. It is a `nx:noop` executor that
depends on all asyncify test group targets, matching the original
definition that was accidentally removed.

## Testing Instructions (or ideally a Blueprint)

Run `npm run fix-asyncify` and verify it no longer errors out with a
missing target.

* Docs: make the PR Preview guide easier to get right on the first try (#3525)

cc @fellyph @annezazu — this is a direct response to your feedback about
Claude setting things up wrong when pointed at the Playground docs.
Thank you for flagging it.

## What this changes

The PR Preview guide was comprehensive but had sharp edges that tripped
up both humans and AI assistants on the first attempt. This pass fills
the specific gaps:

- **"This is a regular action, not a reusable workflow"** callout in
*How it works*. The `jobs.<id>.uses:` vs `jobs.<id>.steps.uses:` mixup
is the single most common failure mode, and LLMs pattern-match to the
wrong form constantly because most popular GitHub Actions named
"workflow" are reusable workflows.
- **New "Testing PRs from forks" section** covering
`pull_request_…
This was referenced Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants