Porter les extensions WordPress

Sur cette page

De nombreuses extensions WordPress peuvent être portées vers EmDash. Le modèle diffère—TypeScript plutôt que PHP, hooks plutôt qu’actions/filtres, stockage structuré plutôt que wp_options—mais la plupart des fonctionnalités se mappent clairement.

Évaluation du portage

Toutes les extensions ne valent pas le coup. Évaluez les candidats avant de commencer.

Bons candidats

Champs personnalisés, SEO, traitement de contenu, extensions d’UI d’administration, analytics, partage social, formulaires

Mauvais candidats

Fonctions multisite, intégrations WooCommerce/Gutenberg, extensions qui modifient le cœur de WordPress

Comparaison de structure

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

Correspondance des hooks

WordPress utilise add_action() et add_filter() avec des noms en chaîne. EmDash utilise des hooks typés dans la définition de l’extension.

Hooks de cycle de vie

WordPressEmDashNotes
register_activation_hook()plugin:installUne fois à la première installation
Extension activéeplugin:activateÀ l’activation
Extension désactivéeplugin:deactivateÀ la désactivation
register_uninstall_hook()plugin:uninstallevent.deleteData = choix utilisateur

Hooks de contenu

WordPressEmDashNotes
wp_insert_post_datacontent:beforeSaveRetourner le contenu modifié ou lever une erreur pour annuler
save_postcontent:afterSaveEffets après enregistrement
before_delete_postcontent:beforeDeleteRetourner false pour annuler
deleted_postcontent:afterDeleteNettoyage après suppression

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 média

WordPressEmDashNotes
wp_handle_upload_prefiltermedia:beforeUploadValider ou transformer
add_attachmentmedia:afterUploadAprès téléversement

Correspondance du stockage

API Options → magasin 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");

Tables personnalisées → collections 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,
});

Schéma de réglages

WordPress utilise l’API Settings pour les formulaires d’admin. EmDash utilise un schéma déclaratif qui génère l’UI automatiquement.

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 d’administration

Les pages d’admin WordPress sont en PHP. EmDash utilise des composants 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>;
	},
};

Enregistrez dans la définition de l’extension :

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

REST API → routes d’extension

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 };
        },
    },
},

Les routes sont sous /_emdash/api/plugins/{plugin-id}/{route-name}.

Processus de portage

  1. Analyser l’extension WordPress

    Documentez hooks, accès base de données, pages admin, endpoints REST.

  2. Mapper vers EmDash

    Hooks WordPress → hooks EmDash. wp_optionsctx.kv. Tables perso → collections storage. Pages admin → React. REST → routes d’extension.

  3. Créer le squelette

    import { definePlugin } from "emdash";
    
    export function createPlugin() {
    	return definePlugin({
    		id: "my-ported-plugin",
    		version: "1.0.0",
    		capabilities: [],
    		storage: {},
    		hooks: {},
    		routes: {},
    		admin: {},
    	});
    }
  4. Implémenter dans l’ordre

    Storage → Hooks → UI admin → Routes

  5. Tester en profondeur

    Vérifiez l’exécution des hooks, le storage et le rendu de l’UI.

Exemple : extension temps de lecture

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

Les extensions doivent déclarer les capabilities requises pour le bac à sable :

CapabilityFournitCas d’usage
network:fetchctx.http.fetch()Appels API externes
read:contentctx.content.get(), list()Lire le contenu CMS
write:contentctx.content.create(), etc.Modifier le contenu
read:mediactx.media.get(), list()Lire les médias
write:mediactx.media.getUploadUrl()Téléverser des médias

Pièges courants

Pas d’état global — Utilisez le storage plutôt que des variables globales.

Tout est asynchrone — Toujours await pour storage et API.

Pas de SQL direct — Utilisez les collections de storage structurées.

Pas de système de fichiers — Utilisez l’API média pour les fichiers.

Étapes suivantes