Portar plugins de WordPress

En esta página

Muchos plugins de WordPress se pueden portar a EmDash. El modelo de plugin es distinto—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 con claridad.

Evaluación de portabilidad

No todos los plugins merecen portarse. Evalúa candidatos antes de empezar.

Buenos candidatos

Campos personalizados, plugins SEO, procesadores de contenido, extensiones de la UI de administración, analítica, redes sociales, formularios

Malos candidatos

Funciones multisitio, integraciones WooCommerce/Gutenberg, plugins que parchean el núcleo de WordPress

Comparación de estructura del plugin

WordPress

wp-content/plugins/my-plugin/
├── my-plugin.php       # Main file with plugin header
├── includes/
│   ├── class-admin.php
│   └── class-api.php
└── admin/
    └── js/

EmDash

my-plugin/
├── src/
│   ├── index.ts    # Plugin definition (definePlugin)
│   └── admin.tsx   # Admin UI exports (React)
├── package.json
└── tsconfig.json

Mapeo de hooks

WordPress usa add_action() y add_filter() con nombres de hook en cadena. 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 activadoplugin:activateAl activarse
Plugin desactivadoplugin:deactivateAl desactivarse
register_uninstall_hook()plugin:uninstallevent.deleteData indica elección del usuario

Hooks de contenido

WordPressEmDashNotas
wp_insert_post_datacontent:beforeSaveDevolver contenido modificado o lanzar error para cancelar
save_postcontent:afterSaveEfectos secundarios tras guardar
before_delete_postcontent:beforeDeleteDevolver false para cancelar
deleted_postcontent:afterDeleteLimpieza tras borrar

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 tras subir

Mapeo de almacenamiento

API de opciones → almacén KV

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 storage

WordPress

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

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

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

EmDash

// Declare in plugin definition
storage: {
    items: {
        indexes: ["status", "createdAt"],
    },
},

// In hooks or 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 ajustes

WordPress usa la Settings API para formularios de administración. EmDash usa un esquema declarativo que genera la 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: "Your API key from the dashboard",
        },
        enabled: {
            type: "boolean",
            label: "Enabled",
            default: true,
        },
        limit: {
            type: "number",
            label: "Item Limit",
            default: 100,
            min: 1,
            max: 1000,
        },
    },
}

UI de administración

Las páginas de administración de WordPress son PHP. EmDash usa componentes 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 items: {count}</div>;
	},
};

export const pages = {
	settings: function SettingsPage() {
		// React component for settings page
		return <div>Settings content</div>;
	},
};

Regístralos en la definición del plugin:

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

REST API → rutas del 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 en /_emdash/api/plugins/{plugin-id}/{route-name}.

Proceso de portado

  1. Analiza el plugin de WordPress

    Documenta hooks, acceso a base de datos, páginas de administración y endpoints REST.

  2. Mapea a conceptos EmDash

    Hooks WordPress → hooks EmDash. wp_optionsctx.kv. Tablas propias → colecciones de storage. Páginas admin → componentes React. REST → rutas del plugin.

  3. Crea 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. Implementa en este orden

    Storage → Hooks → UI de administración → Rutas

  5. Prueba a fondo

    Comprueba que los hooks se disparan, el storage funciona y la UI de admin se renderiza.

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: "Words per minute",
                    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 };
            },
        },
    });
}

Capabilities

Los plugins deben declarar las capabilities necesarias para el sandbox de seguridad:

CapabilityProporcionaCaso de uso
network:fetchctx.http.fetch()Llamadas a APIs externas
read:contentctx.content.get(), list()Leer contenido del CMS
write:contentctx.content.create(), etc.Modificar contenido
read:mediactx.media.get(), list()Leer medios
write:mediactx.media.getUploadUrl()Subir medios

Problemas frecuentes

Sin estado global — Usa storage en lugar de variables globales.

Todo es asíncrono — Siempre await en storage y llamadas API.

Sin SQL directo — Usa colecciones de storage estructuradas.

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

Próximos pasos