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
| WordPress | EmDash | Notas |
|---|---|---|
register_activation_hook() | plugin:install | Se ejecuta una vez en la primera instalación |
| Plugin habilitado | plugin:activate | Se ejecuta cuando se habilita |
| Plugin deshabilitado | plugin:deactivate | Se ejecuta cuando se deshabilita |
register_uninstall_hook() | plugin:uninstall | event.deleteData indica la elección del usuario |
Hooks de Contenido
| WordPress | EmDash | Notas |
|---|---|---|
wp_insert_post_data | content:beforeSave | Retorna contenido modificado o lanza error para cancelar |
save_post | content:afterSave | Efectos secundarios después de guardar |
before_delete_post | content:beforeDelete | Retorna false para cancelar |
deleted_post | content:afterDelete | Limpieza 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
| WordPress | EmDash | Notas |
|---|---|---|
wp_handle_upload_prefilter | media:beforeUpload | Validar o transformar |
add_attachment | media:afterUpload | Reaccionar 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
-
Analizar el plugin de WordPress
Documentar qué hace: hooks, operaciones de base de datos, páginas administrativas, endpoints REST.
-
Mapear a conceptos de EmDash
Hooks de WordPress → Hooks de EmDash.
wp_options→ctx.kv. Tablas personalizadas → Colecciones de almacenamiento. Páginas administrativas → Componentes React. Endpoints REST → Rutas de plugin. -
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: {}, }); } -
Implementar en orden
Almacenamiento → Hooks → UI Administrativa → Rutas
-
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:
| Capacidad | Proporciona | Caso de Uso |
|---|---|---|
network:request | ctx.http.fetch() | Llamadas API externas |
content:read | ctx.content.get(), list() | Leer contenido del CMS |
content:write | ctx.content.create(), etc. | Modificar contenido |
media:read | ctx.media.get(), list() | Leer medios |
media:write | ctx.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
- Hooks — Todos los hooks con firmas
- Storage — Colecciones y consultas
- Settings — Configuraciones respaldadas por KV via Block Kit
- React Admin Pages & Widgets — Construir páginas administrativas (plugins nativos)