Skip to content

Comments

feat: Add local WordPress environment powered by wp-env#328

Open
dcalhoun wants to merge 36 commits intotrunkfrom
feat/local-wordpress-wp-env
Open

feat: Add local WordPress environment powered by wp-env#328
dcalhoun wants to merge 36 commits intotrunkfrom
feat/local-wordpress-wp-env

Conversation

@dcalhoun
Copy link
Member

@dcalhoun dcalhoun commented Feb 19, 2026

What?

Adds a local WordPress development environment powered by @wordpress/env for testing the full editor experience with theme styles, media uploads, and plugin block assets.

Why?

Close CMM-1265.

Testing the editor against a real WordPress instance is essential for validating theme styles, media workflows, and plugin block rendering. Previously, developers had to set up WordPress manually. This provides a one-command local environment that works with both iOS and Android demo apps.

How?

  • wp-env infrastructure: .wp-env.json config with Twenty Twenty-Five theme, Jetpack blocks plugin, and a CORS mu-plugin for cross-origin API access
  • Setup script (bin/wp-env-setup.sh): Provisions application passwords and outputs credentials for the demo apps
  • Makefile targets: wp-env-start, wp-env-stop, wp-env-clean, wp-env-logs, wp-env-cli
  • Demo app integration: Both iOS and Android demo apps include a "Local WordPress" option that connects to the wp-env site with pre-configured credentials
  • Android mixed content fix: WebViewAssetLoader uses setHttpAllowed(true) when the site is HTTP, so the editor page scheme matches and avoids mixed content errors
  • CORS mu-plugin: Allows appassets.androidplatform.net origins (both HTTP and HTTPS) for Android production builds

Testing Instructions

Tip

Using wp-env requires a running Docker. Tools like Docker Desktop (paid license) and Colima (free) provide tools for to managing this.

  1. Start the local WordPress environment:
    make wp-env-start
  2. Configure the site URL for the Android emulator:
    make wp-env-cli CMD="config set WP_SITEURL http://10.0.2.2:8888"
    make wp-env-cli CMD="config set WP_HOME http://10.0.2.2:8888"
  3. Build and run the Android demo app (production build, no dev server)
  4. Select "Local WordPress" from the editor list
  5. Verify: no mixed content or CORS errors in logcat, theme fonts and styles load correctly
  6. Reset the site URL back to localhost for iOS testing:
    make wp-env-cli CMD="config set WP_SITEURL http://localhost:8888"
    make wp-env-cli CMD="config set WP_HOME http://localhost:8888"
  7. Build and run the iOS demo app
  8. Select "Local WordPress" from the editor list
  9. Verify: editor loads correctly with theme styles and no errors
  10. Verify HTTPS sites (e.g., WordPress.com) still work without regression on both platforms

Accessibility Testing Instructions

N/A — no user-facing UI changes beyond adding a new list item in the demo apps.

Screenshots or screencast

iOS Before iOS After
ios-before demo-ios
Android Before Android After
android-before demo-android

dcalhoun and others added 27 commits February 19, 2026 15:54
Add @wordpress/env and the supporting configuration for a local
WordPress development environment. This gives developers a zero-config
way to run the full editor experience locally, including theme styles,
media uploads, and plugin block assets.

- .wp-env.json: Configures WordPress with the Gutenberg plugin and
  CORS mu-plugin
- wp-env/mu-plugins/gutenbergkit-cors.php: Adds CORS headers for
  Vite dev/preview servers and native WebViews
- bin/wp-env-setup.sh: Auto-provisions application password
  credentials after wp-env start
- .gitignore: Excludes the generated credentials file
- package.json: Adds @wordpress/env as a dev dependency
Add wp-env-start, wp-env-stop, wp-env-clean, wp-env-logs, and
wp-env-cli targets for managing the local WordPress environment.
Add a "Local WordPress" entry that reads wp-env credentials and
connects to the local environment. Shows a helpful error message with
setup instructions when wp-env is not running.

- ConfigurationItem: Add .localWordPress case and credentials loader
- EditorList: Add "Local Development" section
- SitePreparationView: Handle .localWordPress in startLoading()
- Xcode scheme: Add WP_ENV_CREDENTIALS_PATH environment variable
Add a "Local WordPress" entry that reads wp-env credentials and
connects to the local environment. Automatically remaps localhost to
10.0.2.2 for Android emulator access.

- ConfigurationItem: Add LocalWordPress sealed class object
- LocalWordPressCredentials: Load and parse credentials file with
  localhost-to-10.0.2.2 remapping
- MainActivity: Add Local WordPress to configurations list and UI
- SitePreparationActivity: Handle LocalWordPress serialization
- SitePreparationViewModel: Handle LocalWordPress in startLoading()
- build.gradle.kts: Add WP_ENV_CREDENTIALS_PATH build config field
- strings.xml: Add Local WordPress string resources
- AGENTS.md: Add Local WordPress Environment section with make targets
- docs/code/local-wordpress.md: Comprehensive guide covering setup,
  platform notes, and troubleshooting
Add a "wp-env" npm script and update the Makefile targets and setup
script to use `npm run wp-env` instead of `npx wp-env`, aligning with
how the Gutenberg repo invokes wp-env.
- Use ?rest_route=/ for the health check since pretty permalinks may
  not be active when wp-env first starts
- Flush rewrite rules after readiness check so /wp-json/ works for
  the demo apps
- Use npm run --silent to prevent npm banner output from leaking into
  the captured application password
…ation

Make cannot pass --flags to recipe scripts. Switch to RESET=1
environment variable, consistent with the existing REFRESH_DEPS=1
and REFRESH_L10N=1 patterns.

Usage: make wp-env-start RESET=1
- EditorList (iOS): Show dynamic footer text — "Not configured" with
  make command hint when credentials file is missing
- SitePreparationView (iOS): Use ContentUnavailableView for errors,
  catch URLError for connection failures with a specific message
- AppError: Make errorDescription optional to satisfy LocalizedError
  protocol correctly, fixing the generic error display
- ConfigurationCard (Android): Show dynamic subtitle based on
  credentials availability
- SitePreparationViewModel (Android): Catch ConnectException with a
  specific message when wp-env server is unreachable
- EditorList: Use neutral footer text that doesn't imply the server
  is running just because a credentials file exists
- SitePreparationView: Catch all URLError types (not just a subset)
  to handle cannotFindHost and other connection failures
Add explicit import for java.util.Properties and move the local
properties loading to a top-level val before the android block.
The LocalWordPress item was added to the configurations list but the
LazyColumn only rendered BundledEditor and ConfiguredEditor instances
explicitly. Add a dedicated item block for LocalWordPress.
The Android emulator cannot access host filesystem paths at runtime.
Instead of reading a file path from local.properties, read the
credentials JSON at build time in Gradle and inject the values as
BuildConfig fields (WP_ENV_SITE_URL, WP_ENV_SITE_API_ROOT,
WP_ENV_AUTH_HEADER).

Developers need to rebuild the app after running make wp-env-start
for credential changes to take effect.
org.json is not available on the Gradle classpath. Use Groovy's
built-in JsonSlurper which is always available in Gradle builds.
WpLoginClient.apiDiscovery() forces HTTPS, which causes an SSL error
when connecting to the HTTP-only local WordPress server. Add a direct
HTTP fallback that fetches the REST API root and inspects the routes
object, matching the approach used by the iOS implementation.
The Android emulator remaps localhost to 10.0.2.2, so the WebView's
origin is http://10.0.2.2:5173 instead of http://localhost:5173. Add
the 10.0.2.2 equivalents to the allowed CORS origins.
WordPress generates image URLs using its configured site URL
(localhost:8888), which doesn't resolve inside the Android emulator.
Document the WP-CLI commands to toggle the site URL between localhost
and 10.0.2.2 for Android testing. Also fix outdated references to
--reset flag and local.properties.
The `gutenberg.latest-stable.zip` URL caused wp-env to extract the
plugin into a `gutenberg.latest-stable/` directory, because wp-env
only strips numeric version suffixes from zip filenames. This broke
Jetpack's editor-assets endpoint, which checks for `/plugins/gutenberg/`
in asset URLs to identify Gutenberg assets. The mismatch caused
Gutenberg-provided scripts like `react` and `react-jsx-runtime` to be
unregistered, silently dropping `jetpack-blocks-editor` due to missing
dependencies.

Using `gutenberg.zip` still fetches the latest stable release but
produces the correct `gutenberg/` directory name.
Install Jetpack alongside Gutenberg in the local WordPress environment
and enable the blocks module during setup. This provides the
`/wpcom/v2/editor-assets` endpoint and additional editor blocks for
testing.

Also gitignore plugin-generated files in `wp-env/mu-plugins/` while
preserving our own CORS mu-plugin.
Rename "Default Editor" to "Standalone Editor" with a clearer description
and merge the "Local Development" and "Editor Configurations" sections
into a single "WordPress Sites" group, since both represent editors
connected to a WordPress site.
Rename "Default editor" to "Standalone editor" with a clearer description,
merge the Local WordPress and Editor Configurations sections into a single
"WordPress Sites" group, and use the Article icon for the standalone editor.
…ld with wp-env

When using the local wp-env WordPress site (HTTP) with the Android
production build, the WebView blocked all HTTP resources as mixed
content because bundled assets were served over HTTPS via
WebViewAssetLoader.

Use `setHttpAllowed(true)` on the asset loader when the configured
site URL uses HTTP, so the editor page scheme matches the site and
eliminates mixed content. Also add the `appassets.androidplatform.net`
origin to the wp-env CORS mu-plugin so REST API and font requests
are allowed from the production build origin.
@dcalhoun dcalhoun added the [Type] Enhancement A suggestion for improvement. label Feb 19, 2026
…dler

The OPTIONS preflight handler used a plain `else` fallback, which sent
`Access-Control-Allow-Origin: *` for any unrecognized origin. Align it
with the REST API handler by using `elseif ( empty( $origin ) )` so
only requests with no Origin header receive the wildcard.
Shell `&&` binds tighter than `||`, so the condition evaluated as
`RESET=true OR (RESET=1 AND file-exists)` instead of the intended
`(RESET=true OR RESET=1) AND file-exists`. Group the OR clause
explicitly with braces.
Document that setHttpAllowed(true) means unencrypted asset traffic,
which is acceptable for local development but not production sites.
Replace `://localhost` instead of bare `localhost` so the substitution
only targets the host portion of the URL, not occurrences in paths or
query parameters.
The allowed-origins array and header-sending logic were duplicated
between the rest_api_init and init handlers. Extract them into
gutenbergkit_cors_allowed_origins() and
gutenbergkit_cors_send_origin_headers() to keep a single source of
truth and prevent the two lists from drifting apart.
@kean
Copy link
Contributor

kean commented Feb 19, 2026

This is looking really nice.

Previously, setHttpAllowed(true) was enabled for any site URL using
the http:// scheme. This could silently downgrade asset traffic to
unencrypted for a non-local site. Restrict the allowance to known
local hosts (localhost, 127.0.0.1, 10.0.2.2) so production sites
always use HTTPS for the asset loader.
The discovery result was logged in the original code but was dropped
when the else branch was rewritten to add the HTTP fallback. Restore
it so the reason for WpLoginClient failure is visible in logcat.
Comment on lines +81 to +86
/**
* Discovers capabilities by directly fetching the REST API root and inspecting
* the `routes` object. This works over plain HTTP, unlike WpLoginClient which
* may require HTTPS.
*/
private fun discoverCapabilitiesViaHttp(siteApiRoot: String): SiteCapabilities {
Copy link
Member Author

@dcalhoun dcalhoun Feb 19, 2026

Choose a reason for hiding this comment

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

Added to circumvent errors related to non-TLS connections when connecting to the local wp-env server. Maybe wordpress-rs should allow this? Maybe not?

uniffi.wp_api.InternalException: UnexpectedUniFFICallbackError(reason: "javax.net.ssl.SSLException: Unable to parse TLS packet header")

Comment on lines +120 to +123
if(this == empty) {
return false
}

Copy link
Member Author

Choose a reason for hiding this comment

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

Cherry picked from #326 to prevent empty bundle load error.

@dcalhoun dcalhoun marked this pull request as ready for review February 19, 2026 22:14
@dcalhoun dcalhoun requested review from adalpari and kean February 19, 2026 22:14
Copy link
Contributor

@kean kean left a comment

Choose a reason for hiding this comment

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

Hey,I got it to work, and the changes look good.

Just to confirm, wordpress/env is a developmemnt-only dependency, correct?

I got a couple of more notes:

  • It required Docker. It's not mentioned in the PR description, but is documented in the new file in docs. All I needed to do was to run it. It then downloaded and ran the image and set up the environment fully automatically.
  • (dev experience) If you don't run a dev server, the editor shows an infinite spinner.
  • (suggestion) It might be worth revisiting the AGENTS.md file, especially in the light of the recent discoveries about how it affects agents performance. I'd suggest moving the basic instructions needed for developers back to README.md and maybe also factoring out some of the coding guidelines and architecture docs.

make test-android
```

### Local WordPress Environment (wp-env)
Copy link
Contributor

Choose a reason for hiding this comment

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

Are agents expected to run these commands for setting up docker and wp-env? If not, it probably doesn't belong to AGENTS.md.

Copy link
Member Author

@dcalhoun dcalhoun Feb 24, 2026

Choose a reason for hiding this comment

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

Long term? Yes, to enable "E2E verification" that allows agents to verify their own work.

Today? Since we do not have an MCP for navigating the product in a simulator/emulator, E2E verification is not really possible. So, there is not really a context where an agent would use these today; it's more relevant to simply use the E2E test commands, which themselves set up the wp-env environment.

I do not believe we should remove this at this time. WDYT?

@RESET=$(RESET) bash bin/wp-env-setup.sh

.PHONY: wp-env-stop
wp-env-stop: ## Stop the local WordPress environment
Copy link
Contributor

Choose a reason for hiding this comment

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

It might be worth calling wp-env directly. You could then use the existing commands and rely on the existing documentation for it: https://developer.wordpress.org/block-editor/reference-guides/packages/packages-env/.

Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean run npm run wp-env rather than having bespoke make targets for interfacing with wp-env? If so, yes, the inability to easily pass npm script arguments is a downside of the make target approach.

That said, my approach so far with the Makefile is creating a simple interface for the project's common commands. One can always run and access the lower-level commands—npm, swift, kotlin—as necessary or desired.

If you find this approach confusing, I'm happy to revisit this, but I suggest we do so holistically in a separate pull request—potentially removing any/all make targets deemed superfluous or confusing.

WDYT?

Copy link
Member Author

@dcalhoun dcalhoun left a comment

Choose a reason for hiding this comment

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

@kean thank you for reviewing and testing.

Just to confirm, wordpress/env is a developmemnt-only dependency, correct?

Yes, it is listed beneath devDependencies in the package.json.

It required Docker. It's not mentioned in the PR description, but is documented in the new file in docs. All I needed to do was to run it. It then downloaded and ran the image and set up the environment fully automatically.

Apologies for not noting it in the PR description. I do not interpret this as actionable, but let me know if I am mistaken. Update: I added a note the PR description.

(dev experience) If you don't run a dev server, the editor shows an infinite spinner.

This is somewhat separate from the wp-env environment. In the future, we could explore adding page load error handlers to the WebView to better surface the inability to load the targeted web page (i.e., the editor HTML).

(suggestion) It might be worth revisiting the AGENTS.md file, especially in the light of the recent discoveries about how it affects agents performance. I'd suggest moving the basic instructions needed for developers back to README.md and maybe also factoring out some of the coding guidelines and architecture docs.

I'd love to hear more about your ideas. Given this feels a bit off topic from the focus of this PR, maybe we continue in chat.

make test-android
```

### Local WordPress Environment (wp-env)
Copy link
Member Author

@dcalhoun dcalhoun Feb 24, 2026

Choose a reason for hiding this comment

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

Long term? Yes, to enable "E2E verification" that allows agents to verify their own work.

Today? Since we do not have an MCP for navigating the product in a simulator/emulator, E2E verification is not really possible. So, there is not really a context where an agent would use these today; it's more relevant to simply use the E2E test commands, which themselves set up the wp-env environment.

I do not believe we should remove this at this time. WDYT?

@RESET=$(RESET) bash bin/wp-env-setup.sh

.PHONY: wp-env-stop
wp-env-stop: ## Stop the local WordPress environment
Copy link
Member Author

Choose a reason for hiding this comment

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

Do you mean run npm run wp-env rather than having bespoke make targets for interfacing with wp-env? If so, yes, the inability to easily pass npm script arguments is a downside of the make target approach.

That said, my approach so far with the Makefile is creating a simple interface for the project's common commands. One can always run and access the lower-level commands—npm, swift, kotlin—as necessary or desired.

If you find this approach confusing, I'm happy to revisit this, but I suggest we do so holistically in a separate pull request—potentially removing any/all make targets deemed superfluous or confusing.

WDYT?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Type] Enhancement A suggestion for improvement.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants