Portar Plugins de WordPress

En esta página

Muchos plugins de WordPress pueden portarse a EmDash. El modelo de plugin es diferente—TypeScript en lugar de PHP, hooks en lugar de actions/filters, almacenamiento estructurado en lugar de wp_options—pero la mayoría de la funcionalidad se mapea limpiamente.

Evaluación de Portabilidad

No todos los plugins tienen sentido portarlos. Evalúe los candidatos antes de comenzar.

Buenos candidatos

Campos personalizados, plugins SEO, procesadores de contenido, extensiones de UI administrativa, analytics, compartir en redes sociales, formularios

Malos candidatos

Características multisitio, integraciones WooCommerce/Gutenberg, plugins que parchean las partes internas del núcleo de WordPress

Comparación de Estructura de Plugins

WordPress

wp-content/plugins/my-plugin/
├── my-plugin.php       # Archivo principal con encabezado del plugin
├── includes/
│   ├── class-admin.php
│   └── class-api.php
└── admin/
    └── js/

EmDash

my-plugin/
├── src/
│   ├── index.ts    # Definición del plugin (definePlugin)
│   └── admin.tsx   # Exportaciones de UI administrativa (React)
├── package.json
└── tsconfig.json

Mapeo de Hooks

WordPress usa add_action() y add_filter() con nombres de hooks en string. EmDash usa hooks tipados declarados en la definición del plugin.

Hooks de Ciclo de Vida

WordPressEmDashNotas
register_activation_hook()plugin:installSe ejecuta una vez en la primera instalación
Plugin habilitadoplugin:activateSe ejecuta cuando se habilita
Plugin deshabilitadoplugin:deactivateSe ejecuta cuando se deshabilita
register_uninstall_hook()plugin:uninstallevent.deleteData indica la elección del usuario

Hooks de Contenido

WordPressEmDashNotas
wp_insert_post_datacontent:beforeSaveRetorna contenido modificado o lanza error para cancelar
save_postcontent:afterSaveEfectos secundarios después de guardar
before_delete_postcontent:beforeDeleteRetorna false para cancelar
deleted_postcontent:afterDeleteLimpieza después de la eliminación

WordPress

add_action('save_post', function($post_id, $post, $update) {
    if ($post->post_type !== 'product') return;

    $price = get_post_meta($post_id, 'price', true);
    if ($price > 1000) {
        update_post_meta($post_id, 'is_premium', true);
    }

}, 10, 3);

EmDash

hooks: {
    "content:afterSave": async (event, ctx) => {
        if (event.collection !== "products") return;

        const price = event.content.price as number;
        if (price > 1000) {
            await ctx.kv.set(`premium:${event.content.id}`, true);
        }
    },
}

Hooks de Medios

WordPressEmDashNotas
wp_handle_upload_prefiltermedia:beforeUploadValidar o transformar
add_attachmentmedia:afterUploadReaccionar después de subir

Mapeo de Almacenamiento

Options API → KV Store

WordPress

$api_key = get_option('my_plugin_api_key', '');
update_option('my_plugin_api_key', 'abc123');
delete_option('my_plugin_api_key');

EmDash

const apiKey = await ctx.kv.get<string>("settings:apiKey") ?? "";
await ctx.kv.set("settings:apiKey", "abc123");
await ctx.kv.delete("settings:apiKey");

Tablas Personalizadas → Colecciones de Almacenamiento

WordPress

global $wpdb;
$table = $wpdb->prefix . 'my_plugin_items';

// Insertar
$wpdb->insert($table, ['name' => 'Item 1', 'status' => 'active']);

// Consultar
$items = $wpdb->get_results(
"SELECT \* FROM $table WHERE status = 'active' LIMIT 10"
);

EmDash

// Declarar en definición del plugin
storage: {
    items: {
        indexes: ["status", "createdAt"],
    },
},

// En hooks o routes:
await ctx.storage.items.put("item-1", {
    name: "Item 1",
    status: "active",
    createdAt: new Date().toISOString(),
});

const result = await ctx.storage.items.query({
    where: { status: "active" },
    limit: 10,
});

Esquema de Configuración

WordPress usa la API de Settings para formularios administrativos. EmDash usa un esquema declarativo que genera UI automáticamente.

WordPress

add_action('admin_init', function() {
    register_setting('my_plugin', 'my_plugin_api_key');
    add_settings_section('main', 'Settings', null, 'my-plugin');
    add_settings_field('api_key', 'API Key', function() {
        $value = get_option('my_plugin_api_key');
        echo '<input type="text" name="my_plugin_api_key"
              value="' . esc_attr($value) . '">';
    }, 'my-plugin', 'main');
});

EmDash

admin: {
    settingsSchema: {
        apiKey: {
            type: "secret",
            label: "API Key",
            description: "Su clave API del panel de control",
        },
        enabled: {
            type: "boolean",
            label: "Habilitado",
            default: true,
        },
        limit: {
            type: "number",
            label: "Límite de Items",
            default: 100,
            min: 1,
            max: 1000,
        },
    },
}

UI Administrativa

Las páginas de administración de WordPress son PHP. EmDash usa componentes de React.

import { useState, useEffect } from "react";

export const widgets = {
	summary: function SummaryWidget() {
		const [count, setCount] = useState(0);

		useEffect(() => {
			fetch("/_emdash/api/plugins/my-plugin/status")
				.then((r) => r.json())
				.then((data) => setCount(data.count));
		}, []);

		return <div>Total de items: {count}</div>;
	},
};

export const pages = {
	settings: function SettingsPage() {
		// Componente React para página de configuración
		return <div>Contenido de configuración</div>;
	},
};

Registrar en la definición del plugin:

admin: {
    entry: "@my-org/my-plugin/admin",
    pages: [{ path: "/settings", label: "Panel" }],
    widgets: [{ id: "summary", title: "Resumen", size: "half" }],
},

REST API → Rutas de Plugin

WordPress

register_rest_route('my-plugin/v1', '/items', [
    'methods' => 'GET',
    'callback' => function($request) {
        global $wpdb;
        $items = $wpdb->get_results("SELECT * FROM items LIMIT 50");
        return new WP_REST_Response($items);
    },
]);

EmDash

routes: {
    items: {
        handler: async (ctx) => {
            const result = await ctx.storage.items.query({ limit: 50 });
            return { items: result.items };
        },
    },
},

Las rutas están disponibles en /_emdash/api/plugins/{plugin-id}/{route-name}.

Proceso de Portado

  1. Analizar el plugin de WordPress

    Documentar qué hace: hooks, operaciones de base de datos, páginas administrativas, endpoints REST.

  2. Mapear a conceptos de EmDash

    Hooks de WordPress → Hooks de EmDash. wp_optionsctx.kv. Tablas personalizadas → Colecciones de almacenamiento. Páginas administrativas → Componentes React. Endpoints REST → Rutas de plugin.

  3. Crear el esqueleto del plugin

    import { definePlugin } from "emdash";
    
    export function createPlugin() {
    	return definePlugin({
    		id: "my-ported-plugin",
    		version: "1.0.0",
    		capabilities: [],
    		storage: {},
    		hooks: {},
    		routes: {},
    		admin: {},
    	});
    }
  4. Implementar en orden

    Almacenamiento → Hooks → UI Administrativa → Rutas

  5. Probar exhaustivamente

    Verificar que los hooks se disparen correctamente, el almacenamiento funcione y la UI administrativa se renderice.

Ejemplo: Plugin de Tiempo de Lectura

WordPress

add_filter('wp_insert_post_data', function($data, $postarr) {
    if ($data['post_type'] !== 'post') return $data;

    $content = strip_tags($data['post_content']);
    $word_count = str_word_count($content);
    $read_time = ceil($word_count / 200);

    if (!empty($postarr['ID'])) {
        update_post_meta($postarr['ID'], '_read_time', $read_time);
    }
    return $data;

}, 10, 2);

EmDash

export function createPlugin() {
    return definePlugin({
        id: "read-time",
        version: "1.0.0",

        admin: {
            settingsSchema: {
                wordsPerMinute: {
                    type: "number",
                    label: "Palabras por minuto",
                    default: 200,
                    min: 100,
                    max: 400,
                },
            },
        },

        hooks: {
            "content:beforeSave": async (event, ctx) => {
                if (event.collection !== "posts") return;

                const wpm = await ctx.kv.get<number>("settings:wordsPerMinute") ?? 200;
                const text = JSON.stringify(event.content.body || "");
                const readTime = Math.ceil(text.split(/\s+/).length / wpm);

                return { ...event.content, readTime };
            },
        },
    });
}

Capacidades

Los plugins deben declarar las capacidades requeridas para el sandboxing de seguridad:

CapacidadProporcionaCaso de Uso
network:requestctx.http.fetch()Llamadas API externas
content:readctx.content.get(), list()Leer contenido del CMS
content:writectx.content.create(), etc.Modificar contenido
media:readctx.media.get(), list()Leer medios
media:writectx.media.getUploadUrl()Subir medios

Errores Comunes

Sin estado global — Use almacenamiento en lugar de variables globales.

Todo asíncrono — Siempre use await en llamadas de almacenamiento y API.

Sin SQL directo — Use colecciones de almacenamiento estructuradas.

Sin sistema de archivos — Use la API de medios para archivos.

Próximos Pasos