Muitos plugins do WordPress podem ser portados para o EmDash. O modelo de plugin é diferente—TypeScript em vez de PHP, hooks em vez de actions/filters, armazenamento estruturado em vez de wp_options—mas a maioria das funcionalidades mapeia de forma limpa.
Avaliação de Portabilidade
Nem todos os plugins fazem sentido portar. Avalie os candidatos antes de começar.
Bons candidatos
Campos personalizados, plugins SEO, processadores de conteúdo, extensões de UI administrativa, analytics, compartilhamento social, formulários
Candidatos inadequados
Recursos multisite, integrações WooCommerce/Gutenberg, plugins que fazem patch nos componentes internos do núcleo do WordPress
Comparação de Estrutura de Plugins
WordPress
wp-content/plugins/my-plugin/
├── my-plugin.php # Arquivo principal com cabeçalho do plugin
├── includes/
│ ├── class-admin.php
│ └── class-api.php
└── admin/
└── js/ EmDash
my-plugin/
├── src/
│ ├── index.ts # Definição do plugin (definePlugin)
│ └── admin.tsx # Exports de UI administrativa (React)
├── package.json
└── tsconfig.json Mapeamento de Hooks
O WordPress usa add_action() e add_filter() com nomes de hooks em string. O EmDash usa hooks tipados declarados na definição do plugin.
Hooks de Ciclo de Vida
| WordPress | EmDash | Notas |
|---|---|---|
register_activation_hook() | plugin:install | Executa uma vez na primeira instalação |
| Plugin habilitado | plugin:activate | Executa quando habilitado |
| Plugin desabilitado | plugin:deactivate | Executa quando desabilitado |
register_uninstall_hook() | plugin:uninstall | event.deleteData indica escolha do usuário |
Hooks de Conteúdo
| WordPress | EmDash | Notas |
|---|---|---|
wp_insert_post_data | content:beforeSave | Retorna conteúdo modificado ou lança erro para cancelar |
save_post | content:afterSave | Efeitos colaterais após salvar |
before_delete_post | content:beforeDelete | Retorne false para cancelar |
deleted_post | content:afterDelete | Limpeza após exclusão |
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 Mídia
| WordPress | EmDash | Notas |
|---|---|---|
wp_handle_upload_prefilter | media:beforeUpload | Validar ou transformar |
add_attachment | media:afterUpload | Reagir após upload |
Mapeamento de Armazenamento
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"); Tabelas Personalizadas → Coleções de Armazenamento
WordPress
global $wpdb;
$table = $wpdb->prefix . 'my_plugin_items';
// Inserir
$wpdb->insert($table, ['name' => 'Item 1', 'status' => 'active']);
// Consultar
$items = $wpdb->get_results(
"SELECT \* FROM $table WHERE status = 'active' LIMIT 10"
);
EmDash
// Declarar na definição do plugin
storage: {
items: {
indexes: ["status", "createdAt"],
},
},
// Em hooks ou 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,
}); Schema de Configurações
O WordPress usa a API Settings para formulários administrativos. O EmDash usa um schema declarativo que gera UI automaticamente.
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: "Chave API",
description: "Sua chave API do painel",
},
enabled: {
type: "boolean",
label: "Habilitado",
default: true,
},
limit: {
type: "number",
label: "Limite de Itens",
default: 100,
min: 1,
max: 1000,
},
},
} UI Administrativa
As páginas administrativas do WordPress são PHP. O 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 de itens: {count}</div>;
},
};
export const pages = {
settings: function SettingsPage() {
// Componente React para página de configurações
return <div>Conteúdo de configurações</div>;
},
};
Registrar na definição do plugin:
admin: {
entry: "@my-org/my-plugin/admin",
pages: [{ path: "/settings", label: "Painel" }],
widgets: [{ id: "summary", title: "Resumo", size: "half" }],
},
REST API → Rotas 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 };
},
},
}, As rotas estão disponíveis em /_emdash/api/plugins/{plugin-id}/{route-name}.
Processo de Portabilidade
-
Analisar o plugin do WordPress
Documente o que ele faz: hooks, operações de banco de dados, páginas administrativas, endpoints REST.
-
Mapear para conceitos do EmDash
Hooks do WordPress → Hooks do EmDash.
wp_options→ctx.kv. Tabelas personalizadas → Coleções de armazenamento. Páginas administrativas → Componentes React. Endpoints REST → Rotas de plugin. -
Criar o esqueleto do plugin
import { definePlugin } from "emdash"; export function createPlugin() { return definePlugin({ id: "my-ported-plugin", version: "1.0.0", capabilities: [], storage: {}, hooks: {}, routes: {}, admin: {}, }); } -
Implementar na ordem
Armazenamento → Hooks → UI Administrativa → Rotas
-
Testar minuciosamente
Verifique se os hooks disparam corretamente, o armazenamento funciona e a UI administrativa renderiza.
Exemplo: Plugin de Tempo de Leitura
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: "Palavras 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
Os plugins devem declarar as capacidades necessárias para sandboxing de segurança:
| Capacidade | Fornece | Caso de Uso |
|---|---|---|
network:request | ctx.http.fetch() | Chamadas de API externas |
content:read | ctx.content.get(), list() | Leitura de conteúdo CMS |
content:write | ctx.content.create(), etc. | Modificação de conteúdo |
media:read | ctx.media.get(), list() | Leitura de mídia |
media:write | ctx.media.getUploadUrl() | Upload de mídia |
Armadilhas Comuns
Sem estado global — Use armazenamento em vez de variáveis globais.
Tudo assíncrono — Sempre use await em chamadas de armazenamento e API.
Sem SQL direto — Use coleções de armazenamento estruturadas.
Sem sistema de arquivos — Use a API de mídia para arquivos.
Próximos Passos
- Hooks — Todos os hooks com assinaturas
- Storage — Coleções e consultas
- Settings — Configurações baseadas em KV via Block Kit
- React Admin Pages & Widgets — Construindo páginas administrativas (plugins nativos)