Files
ServerKit/backend/app/services/wordpress_bridge.py
T
Juan DenisandClaude Opus 4.8 eee21d8a14 refactor(plugins): add WordPress bridge + flagship/multi-blueprint plumbing
Pre-reqs for extracting WordPress into a bundled default-installed extension
(#38). No behavior change yet — WordPress still ships as core; this only inverts
the couplings so the physical move is mechanical:

- app/services/wordpress_bridge.py: importlib bridge to the (future) dashed
  package app.plugins.serverkit-wordpress, with a two-speed fallback to the
  legacy core location so callers work before AND after the move.
- plugin_service: _ensure_builtin_backend_importable() registers a dashed builtin
  backend in-place via spec_from_file_location (no copy) — solves dev/test/prod
  loadability of a default-installed dashed package; extra_blueprints manifest
  support (WP needs 3 blueprints / 4 mounts incl. the /pipelines alias);
  FLAGSHIP_SLUGS + seed_flagship_extensions() + flagship-aware install/uninstall
  (D4: default-installed, uninstallable, never deletes tracked files).
- create_app: seed flagships before load_all_plugins (inert until the folder exists).
- Re-point the 6 lazy WP-service callers (preview/webhook/env-pipeline/apps/
  builtin_handlers/backup_policy) through the bridge; drop the eager
  WordPressService import from app/services/__init__.py. Models stay core.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-01 23:45:22 -04:00

83 lines
3.4 KiB
Python

"""Importlib bridge to the ``serverkit-wordpress`` extension backend.
WordPress ships as a bundled, default-installed, uninstallable extension (D4).
Its backend lives in ``builtin-extensions/serverkit-wordpress/backend/`` and is
loaded as the *dashed* package ``app.plugins.serverkit-wordpress``. Because the
slug contains a dash it can never be reached with a normal ``import`` statement
(``import app.plugins.serverkit-wordpress`` is a SyntaxError), so the handful of
core call sites that still reach into the WordPress stack go through this bridge,
which resolves modules with ``importlib`` on a *string* path — dash-safe.
Loadability is uniform across prod / dev / test:
* **Copy-installed** (``app/plugins/serverkit-wordpress`` exists) — the ordinary
path-based finder resolves it, same as any marketplace plugin.
* **In-place** (dev checkout, the test suite, or a fresh boot *before* the
flagship seed row is created) — we register the builtin backend directory in
``sys.modules`` as the package via ``spec_from_file_location`` with
``submodule_search_locations`` pointing at it, so both submodule and relative
imports inside the extension resolve. **No file copy is required.** This is
what makes a dashed, default-installed package loadable everywhere.
During the two-speed migration window the bridge also falls back to the legacy
core location (``app.services.<module>``) so callers can be re-pointed *before*
the physical move lands without breaking. Once the files move, the extension
path wins and the fallback is simply never reached.
"""
import importlib
SLUG = 'serverkit-wordpress'
PKG = f'app.plugins.{SLUG}'
def ensure_loadable():
"""Make ``app.plugins.serverkit-wordpress`` importable, in-place if needed.
Delegates to the canonical in-place loader in ``plugin_service`` (shared with
the flagship boot path). Idempotent and cheap after the first call. Returns
``True`` if the package is (now) importable, ``False`` if the extension isn't
present on disk at all (in which case callers fall back to the legacy core
location during the migration window).
"""
from app.services.plugin_service import _ensure_builtin_backend_importable
return _ensure_builtin_backend_importable(SLUG)
def load(module_name):
"""Import a module from the extension backend (e.g. ``'wordpress_service'``).
Falls back to the legacy core location during the migration window.
"""
if ensure_loadable():
try:
return importlib.import_module(f'{PKG}.{module_name}')
except ImportError:
pass
# Two-speed fallback: the file may still live in core (pre-move).
return importlib.import_module(f'app.services.{module_name}')
def get(module_name, attr):
"""Return an attribute (usually a service class) from an extension module."""
return getattr(load(module_name), attr)
# ── Convenience accessors for the service classes core code reaches for ──
# All WordPress *models* stay core (app.models.wordpress_site / wordpress_custom_plugin);
# only these services physically move into the extension, so only these need the bridge.
def wordpress_service():
return get('wordpress_service', 'WordPressService')
def git_wordpress_service():
return get('git_wordpress_service', 'GitWordPressService')
def wordpress_env_service():
return get('wordpress_env_service', 'WordPressEnvService')
def wp_update_service():
return get('wp_update_service', 'WpUpdateService')