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
| WordPress | EmDash | Notas |
|---|---|---|
register_activation_hook() | plugin:install | Se ejecuta una vez en la primera instalación |
| Plugin activado | plugin:activate | Al activarse |
| Plugin desactivado | plugin:deactivate | Al desactivarse |
register_uninstall_hook() | plugin:uninstall | event.deleteData indica elección del usuario |
Hooks de contenido
| WordPress | EmDash | Notas |
|---|---|---|
wp_insert_post_data | content:beforeSave | Devolver contenido modificado o lanzar error para cancelar |
save_post | content:afterSave | Efectos secundarios tras guardar |
before_delete_post | content:beforeDelete | Devolver false para cancelar |
deleted_post | content:afterDelete | Limpieza 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
| WordPress | EmDash | Notas |
|---|---|---|
wp_handle_upload_prefilter | media:beforeUpload | Validar o transformar |
add_attachment | media:afterUpload | Reaccionar 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
-
Analiza el plugin de WordPress
Documenta hooks, acceso a base de datos, páginas de administración y endpoints REST.
-
Mapea a conceptos EmDash
Hooks WordPress → hooks EmDash.
wp_options→ctx.kv. Tablas propias → colecciones de storage. Páginas admin → componentes React. REST → rutas del plugin. -
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: {}, }); } -
Implementa en este orden
Storage → Hooks → UI de administración → Rutas
-
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:
| Capability | Proporciona | Caso de uso |
|---|---|---|
network:fetch | ctx.http.fetch() | Llamadas a APIs externas |
read:content | ctx.content.get(), list() | Leer contenido del CMS |
write:content | ctx.content.create(), etc. | Modificar contenido |
read:media | ctx.media.get(), list() | Leer medios |
write:media | ctx.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
- Hooks Reference — Todos los hooks con firmas
- Storage API — Colecciones y consultas
- Settings — Esquema de ajustes y almacén KV
- Admin UI — Crear páginas de administración