Portar Plugins do WordPress

Nesta página

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

WordPressEmDashNotas
register_activation_hook()plugin:installExecuta uma vez na primeira instalação
Plugin habilitadoplugin:activateExecuta quando habilitado
Plugin desabilitadoplugin:deactivateExecuta quando desabilitado
register_uninstall_hook()plugin:uninstallevent.deleteData indica escolha do usuário

Hooks de Conteúdo

WordPressEmDashNotas
wp_insert_post_datacontent:beforeSaveRetorna conteúdo modificado ou lança erro para cancelar
save_postcontent:afterSaveEfeitos colaterais após salvar
before_delete_postcontent:beforeDeleteRetorne false para cancelar
deleted_postcontent:afterDeleteLimpeza 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

WordPressEmDashNotas
wp_handle_upload_prefiltermedia:beforeUploadValidar ou transformar
add_attachmentmedia:afterUploadReagir 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

  1. Analisar o plugin do WordPress

    Documente o que ele faz: hooks, operações de banco de dados, páginas administrativas, endpoints REST.

  2. Mapear para conceitos do EmDash

    Hooks do WordPress → Hooks do EmDash. wp_optionsctx.kv. Tabelas personalizadas → Coleções de armazenamento. Páginas administrativas → Componentes React. Endpoints REST → Rotas de plugin.

  3. 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: {},
    	});
    }
  4. Implementar na ordem

    Armazenamento → Hooks → UI Administrativa → Rotas

  5. 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:

CapacidadeForneceCaso de Uso
network:requestctx.http.fetch()Chamadas de API externas
content:readctx.content.get(), list()Leitura de conteúdo CMS
content:writectx.content.create(), etc.Modificação de conteúdo
media:readctx.media.get(), list()Leitura de mídia
media:writectx.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