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
| WordPress | EmDash | Notes |
|---|---|---|
register_activation_hook() | plugin:install | Une fois à la première installation |
| Extension activée | plugin:activate | À l’activation |
| Extension désactivée | plugin:deactivate | À la désactivation |
register_uninstall_hook() | plugin:uninstall | event.deleteData = choix utilisateur |
Hooks de contenu
| WordPress | EmDash | Notes |
|---|---|---|
wp_insert_post_data | content:beforeSave | Retourner le contenu modifié ou lever une erreur pour annuler |
save_post | content:afterSave | Effets après enregistrement |
before_delete_post | content:beforeDelete | Retourner false pour annuler |
deleted_post | content:afterDelete | Nettoyage 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
| WordPress | EmDash | Notes |
|---|---|---|
wp_handle_upload_prefilter | media:beforeUpload | Valider ou transformer |
add_attachment | media:afterUpload | Aprè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
-
Analyser l’extension WordPress
Documentez hooks, accès base de données, pages admin, endpoints REST.
-
Mapper vers EmDash
Hooks WordPress → hooks EmDash.
wp_options→ctx.kv. Tables perso → collections storage. Pages admin → React. REST → routes d’extension. -
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: {}, }); } -
Implémenter dans l’ordre
Storage → Hooks → UI admin → Routes
-
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 :
| Capability | Fournit | Cas d’usage |
|---|---|---|
network:fetch | ctx.http.fetch() | Appels API externes |
read:content | ctx.content.get(), list() | Lire le contenu CMS |
write:content | ctx.content.create(), etc. | Modifier le contenu |
read:media | ctx.media.get(), list() | Lire les médias |
write:media | ctx.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
- Hooks Reference — Tous les hooks avec signatures
- Storage API — Collections et requêtes
- Settings — Schéma et magasin KV
- Admin UI — Pages d’administration