Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
8bacfd4
feat: Add wp-env local WordPress environment infrastructure
dcalhoun Feb 19, 2026
bf10818
feat: Add Makefile targets for wp-env lifecycle management
dcalhoun Feb 19, 2026
bad876e
feat: Add Local WordPress option to iOS demo app
dcalhoun Feb 19, 2026
4cf01b0
feat: Add Local WordPress option to Android demo app
dcalhoun Feb 19, 2026
96d25d9
docs: Add wp-env local WordPress documentation
dcalhoun Feb 19, 2026
4266fe6
refactor: Replace npx with npm run for wp-env commands
dcalhoun Feb 19, 2026
fb503f2
chore: Update package-lock.json with @wordpress/env dependency
dcalhoun Feb 19, 2026
9f9bb44
fix: Fix wp-env setup script health check and npm output leaking
dcalhoun Feb 19, 2026
d68d83a
fix: Use RESET env var instead of --reset flag for credential regener…
dcalhoun Feb 19, 2026
078d60f
fix: Improve Local WordPress status feedback in demo apps
dcalhoun Feb 19, 2026
a4abfc9
fix: Improve Local WordPress status messaging in iOS demo
dcalhoun Feb 19, 2026
8ba8ef6
fix: Fix Android build.gradle.kts unresolved reference error
dcalhoun Feb 19, 2026
1a5f3f7
fix: Add LocalWordPress branch to ConfigurationAdapter exhaustive when
dcalhoun Feb 19, 2026
2fa9100
fix: Render Local WordPress card in Android demo app main screen
dcalhoun Feb 19, 2026
5fbcf32
fix: Read wp-env credentials at build time for Android
dcalhoun Feb 19, 2026
211db56
fix: Use groovy.json.JsonSlurper for Gradle JSON parsing
dcalhoun Feb 19, 2026
c570e4c
fix: Add HTTP fallback for site capability discovery on Android
dcalhoun Feb 19, 2026
41394d8
FIx `hasAssetData` assertion for empty bundle
jkmassel Feb 18, 2026
7b5a3c9
fix: Allow Android emulator origins in CORS mu-plugin
dcalhoun Feb 19, 2026
a5fa298
docs: Document Android emulator image URL workaround
dcalhoun Feb 19, 2026
51ee10f
fix: Use version-agnostic Gutenberg zip URL in wp-env config
dcalhoun Feb 19, 2026
aed566e
feat: Add Jetpack plugin with blocks module to wp-env
dcalhoun Feb 19, 2026
6a7868e
refactor: Relabel and regroup editor list in iOS demo app
dcalhoun Feb 19, 2026
c41411d
refactor: Relabel and regroup editor list in Android demo app
dcalhoun Feb 19, 2026
2749cc7
chore: Remove unused imports from MainActivity
dcalhoun Feb 19, 2026
6e240d5
docs: Remove Docker Desktop reference
dcalhoun Feb 19, 2026
d2dcec8
fix: Resolve mixed content and CORS errors for Android production bui…
dcalhoun Feb 19, 2026
a91d5e2
fix: Restrict CORS wildcard to empty-origin requests in preflight han…
dcalhoun Feb 19, 2026
c3685ff
fix: Correct operator precedence in RESET conditional
dcalhoun Feb 19, 2026
8aaa756
docs: Add security note for HTTP asset loader trade-off
dcalhoun Feb 19, 2026
7c8773d
fix: Scope localhost remap to URL scheme to avoid false matches
dcalhoun Feb 19, 2026
5d74500
refactor: Extract shared CORS allowed-origins list into helper functions
dcalhoun Feb 19, 2026
deb0034
fix: Restrict HTTP asset loading to known local development hosts
dcalhoun Feb 19, 2026
8078719
fix: Reinstate apiDiscoveryResult value in fallback log message
dcalhoun Feb 19, 2026
d812fcb
docs: Remove Docker Desktop reference in favor of generic Docker
dcalhoun Feb 19, 2026
8d56cc4
docs: Add local WordPress link to code contributions index
dcalhoun Feb 19, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -200,5 +200,10 @@ src/translations/
e2e/test-results/
playwright-report/

# wp-env
.wp-env.credentials.json
wp-env/mu-plugins/*
!wp-env/mu-plugins/gutenbergkit-cors.php

# Claude
.claude/settings.local.json
13 changes: 13 additions & 0 deletions .wp-env.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"plugins": [
"https://downloads.wordpress.org/plugin/gutenberg.zip",
"https://downloads.wordpress.org/plugin/jetpack.zip"
],
"mappings": {
"wp-content/mu-plugins": "./wp-env/mu-plugins"
},
"config": {
"WP_DEBUG": true,
"WP_DEBUG_LOG": true
}
}
23 changes: 23 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,29 @@ make local-android-library
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?


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

```bash
# Start the local WordPress environment (requires Docker)
make wp-env-start

# Stop the environment (preserves data)
make wp-env-stop

# Destroy the environment and remove all data
make wp-env-clean

# View WordPress logs
make wp-env-logs

# Run a WP-CLI command
make wp-env-cli CMD="post list"
```

See `docs/code/local-wordpress.md` for detailed setup instructions and troubleshooting.

> **Note:** Most `make` targets have equivalent `npm` scripts in `package.json`. Build targets accept `REFRESH_DEPS=1`, `REFRESH_L10N=1`, and `REFRESH_JS_BUILD=1` flags to force refresh of dependencies, translations, and JavaScript builds respectively. Run `make help` for full details.

## Architecture
Expand Down
26 changes: 26 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,32 @@ dev-tools: npm-dependencies ## Start the React Developer Tools
preview: npm-dependencies ## Preview the production build locally
npm run preview

################################################################################
# Local WordPress Environment Targets (wp-env)
################################################################################

.PHONY: wp-env-start
wp-env-start: npm-dependencies ## Start the local WordPress environment (requires Docker; RESET=1 to regenerate credentials)
npm run wp-env start
@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
Contributor

Choose a reason for hiding this comment

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

That sounds good.

npm run wp-env stop

.PHONY: wp-env-clean
wp-env-clean: ## Stop wp-env and remove all data (fresh start)
npm run wp-env destroy
@rm -f .wp-env.credentials.json

.PHONY: wp-env-logs
wp-env-logs: ## Show wp-env WordPress logs
npm run wp-env logs

.PHONY: wp-env-cli
wp-env-cli: ## Run a WP-CLI command in wp-env (usage: make wp-env-cli CMD="post list")
npm run wp-env run cli wp $(CMD)

################################################################################
# Code Quality Targets
################################################################################
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ import org.wordpress.gutenberg.model.GBKitGlobal
import org.wordpress.gutenberg.services.EditorService
import java.util.Locale

const val ASSET_URL = "https://appassets.androidplatform.net/assets/index.html"
private const val ASSET_URL = "https://appassets.androidplatform.net/assets/index.html"
private const val ASSET_URL_HTTP = "http://appassets.androidplatform.net/assets/index.html"

/**
* A WebView-based Gutenberg block editor for Android.
Expand Down Expand Up @@ -85,9 +86,7 @@ const val ASSET_URL = "https://appassets.androidplatform.net/assets/index.html"
class GutenbergView : WebView {
private var isEditorLoaded = false
private var didFireEditorLoaded = false
private var assetLoader = WebViewAssetLoader.Builder()
.addPathHandler("/assets/", AssetsPathHandler(this.context))
.build()
private lateinit var assetLoader: WebViewAssetLoader
private val configuration: EditorConfiguration
private lateinit var dependencies: EditorDependencies

Expand Down Expand Up @@ -268,7 +267,7 @@ class GutenbergView : WebView {
}

// Allow asset URLs
if (url.host == Uri.parse(ASSET_URL).host) {
if (url.host == "appassets.androidplatform.net") {
return false
}

Expand Down Expand Up @@ -392,19 +391,31 @@ class GutenbergView : WebView {
configuration.cachedAssetHosts
)

// Build the asset loader. When the site is a local dev server over HTTP,
// serve assets over HTTP too so that Android WebView doesn't block site
// resources as mixed content. Only allow this for known local hosts to
// avoid accidentally downgrading asset traffic for production sites.
val siteUri = Uri.parse(configuration.siteURL)
val isLocalHttpSite = siteUri.scheme == "http" && siteUri.host in LOCAL_HOSTS
assetLoader = WebViewAssetLoader.Builder()
.setHttpAllowed(isLocalHttpSite)
.addPathHandler("/assets/", AssetsPathHandler(this.context))
.build()

// Notify that dependency loading is complete (spinner phase begins)
loadingListener?.onDependencyLoadingFinished()

initializeWebView()

val assetUrl = if (isLocalHttpSite) ASSET_URL_HTTP else ASSET_URL
val editorUrl = BuildConfig.GUTENBERG_EDITOR_URL.ifEmpty {
ASSET_URL
assetUrl
}

WebStorage.getInstance().deleteAllData()
this.clearCache(true)
// All cookies are third-party cookies because the root of this document
// lives under `https://appassets.androidplatform.net`
// lives under `appassets.androidplatform.net`
CookieManager.getInstance().setAcceptThirdPartyCookies(this, true)

// Erase all local cookies before loading the URL – we don't want to persist
Expand Down Expand Up @@ -870,6 +881,9 @@ class GutenbergView : WebView {
}

companion object {
/** Hosts that are safe to serve assets over HTTP (local development only). */
private val LOCAL_HOSTS = setOf("localhost", "127.0.0.1", "10.0.2.2")

private const val ASSET_LOADING_TIMEOUT_MS = 5000L

// Warmup state management
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ data class EditorAssetBundle(
* @return `true` if the asset has been cached locally.
*/
fun hasAssetData(url: String): Boolean {
if(this == empty) {
return false
}

Comment on lines +120 to +123
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.

return assetDataPath(url).exists()
}

Expand Down
21 changes: 21 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,22 @@ plugins {
alias(libs.plugins.jetbrains.kotlin.compose)
}

// Read wp-env credentials at build time. The emulator cannot access host
// filesystem paths at runtime, so we bake the values into BuildConfig.
@Suppress("UNCHECKED_CAST")
val wpEnvCredentials: Map<String, String> = run {
val file = rootProject.file("../.wp-env.credentials.json")
if (file.exists()) {
try {
groovy.json.JsonSlurper().parseText(file.readText()) as Map<String, String>
} catch (_: Exception) {
emptyMap()
}
} else {
emptyMap()
}
}

android {
namespace = "com.example.gutenbergkit"
compileSdk = 34
Expand All @@ -16,6 +32,10 @@ android {
versionName = "1.0"

testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"

buildConfigField("String", "WP_ENV_SITE_URL", "\"${wpEnvCredentials["siteUrl"] ?: ""}\"")
buildConfigField("String", "WP_ENV_SITE_API_ROOT", "\"${wpEnvCredentials["siteApiRoot"] ?: ""}\"")
buildConfigField("String", "WP_ENV_AUTH_HEADER", "\"${wpEnvCredentials["authHeader"] ?: ""}\"")
}

buildTypes {
Expand All @@ -36,6 +56,7 @@ android {
}
buildFeatures {
compose = true
buildConfig = true
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,13 @@ class ConfigurationAdapter(
holder.subtitleText.visibility = View.VISIBLE
}

is ConfigurationItem.LocalWordPress -> {
holder.titleText.text = holder.itemView.context.getString(R.string.local_wordpress)
holder.subtitleText.text =
holder.itemView.context.getString(R.string.local_wordpress_subtitle)
holder.subtitleText.visibility = View.VISIBLE
}

is ConfigurationItem.ConfiguredEditor -> {
holder.titleText.text = item.name
holder.subtitleText.text = item.siteUrl
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.example.gutenbergkit

sealed class ConfigurationItem {
object BundledEditor : ConfigurationItem()
object LocalWordPress : ConfigurationItem()
data class ConfiguredEditor(
val name: String,
val siteUrl: String,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.example.gutenbergkit

data class LocalWordPressCredentials(
val siteUrl: String,
val siteApiRoot: String,
val authHeader: String
) {
companion object {
/**
* Loads credentials from BuildConfig fields populated at build time from
* `.wp-env.credentials.json`. Remaps `localhost` to `10.0.2.2` so the
* Android emulator can reach the host machine.
*/
fun load(): LocalWordPressCredentials? {
val siteUrl = BuildConfig.WP_ENV_SITE_URL
if (siteUrl.isEmpty()) return null

return LocalWordPressCredentials(
siteUrl = remapLocalhost(siteUrl),
siteApiRoot = remapLocalhost(BuildConfig.WP_ENV_SITE_API_ROOT),
authHeader = BuildConfig.WP_ENV_AUTH_HEADER
)
}

private fun remapLocalhost(url: String): String =
url.replace("://localhost", "://10.0.2.2")
}
}
Loading
Loading