etherpad-lite/doc/plugins.adoc
John McLear 7d537f3deb
Require Node.js >= 25 (engines, installers, Dockerfile, snap, CI, docs) (#7749)
* docs: bump documented Node.js minimum to 25

Etherpad is moving its supported Node.js floor to >= 25 (CI matrix is
already pinned to 25 across all workflows on the node25-corepack-pnpm11
work). Sync the user-facing documentation so the install instructions,
requirements section, and plugin metadata example all reflect the new
minimum instead of Node 22 / 12.17.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: require Node.js >= 25 (engines, installers, Dockerfile, snap, CI)

#7747 added Node 25 *support* but left the floor at Node 22. This
commit completes the cutover so the runtime requirement matches the
documentation bumped in the previous commit.

- package.json: engines.node ">=22.13.0" → ">=25.0.0"
- bin/functions.sh, bin/installer.sh, bin/installer.ps1: REQUIRED_NODE
  bumped to 25 (controls the error message users see when they invoke
  the installer or pnpm scripts on an older Node)
- Dockerfile: base image node:22-alpine → node:25-alpine (×2). Corepack
  comment updated: Node 25 no longer ships corepack at all, so we
  install it from npm rather than refreshing a stale signing-key list
- snap/snapcraft.yaml: pinned NODE_VERSION 22.22.2 → 25.9.0 and the
  surrounding design notes rewritten to reflect Node 25 instead of 22
- .github/workflows/*.yml: matrix dropped from [22, 24, 25] to just
  [25] (anything older now fails engines anyway). Stale comments in
  build-and-deploy-docs.yml referencing vite 8's 22.12 floor cleaned up
- bin/plugins/lib/npmpublish.yml: setup-node 22 → 25 so the plugin
  template propagated to every ether/* plugin matches the new minimum

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(docker): install pnpm directly on Node 25 (no corepack)

node:25-alpine doesn't ship corepack but does pre-install yarn at
/usr/local/bin/yarn, so `npm install -g corepack@latest` fails with
EEXIST trying to register its yarn shim. Per #7747, end-users install
pnpm via plain `npm install -g pnpm` on Node 25 — use the same flow in
the Dockerfile (and remove the unused yarn binary so it doesn't sit on
PATH inside the image). Drops COREPACK_HOME and the related
issue-7687 cache-sharing tweak since there's no corepack shim to share.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-15 10:04:24 +01:00

253 lines
9.3 KiB
Text

== Plugins
Etherpad allows you to extend its functionality with plugins. A plugin registers
hooks (functions) for certain events (thus certain features) in Etherpad to
execute its own functionality based on these events.
Publicly available plugins can be found in the npm registry (see
<https://npmjs.org>). Etherpad's naming convention for plugins is to prefix your
plugins with `ep_`. So, e.g. it's `ep_flubberworms`. Thus you can install
plugins from npm, using `npm install --no-save --legacy-peer-deps
ep_flubberworm` in Etherpad's root directory.
You can also browse to `http://yourEtherpadInstan.ce/admin/plugins`, which will
list all installed plugins and those available on npm. It even provides
functionality to search through all available plugins.
=== Folder structure
Ideally a plugin has the following folder structure:
[source]
----
ep_<plugin>/
├ .github/
│ └ workflows/
│ └ npmpublish.yml ◄─ GitHub workflow to auto-publish on push
├ static/
│ ├ css/ ◄─ static .css files
│ ├ images/ ◄─ static image files
│ ├ js/
│ │ └ index.js ◄─ static client-side code
│ └ tests/
│ ├ backend/
│ │ └ specs/ ◄─ backend (server) tests
│ └ frontend/
│ └ specs/ ◄─ frontend (client) tests
├ templates/ ◄─ EJS templates (.html, .js, .css, etc.)
├ locales/
│ ├ en.json ◄─ English (US) strings
│ └ qqq.json ◄─ optional hints for translators
├ .travis.yml ◄─ Travis CI config
├ LICENSE
├ README.md
├ ep.json ◄─ Etherpad plugin definition
├ index.js ◄─ server-side code
├ package.json
└ package-lock.json
----
If your plugin includes client-side hooks, put them in `static/js/`. If you're
adding in CSS or image files, you should put those files in `static/css/ `and
`static/image/`, respectively, and templates go into `templates/`. Translations
go into `locales/`. Tests go in `static/tests/backend/specs/` and
`static/tests/frontend/specs/`.
A Standard directory structure like this makes it easier to navigate through
your code. That said, do note, that this is not actually *required* to make your
plugin run. If you want to make use of our i18n system, you need to put your
translations into `locales/`, though, in order to have them integrated. (See
"Localization" for more info on how to localize your plugin.)
=== Plugin definition
Your plugin definition goes into `ep.json`. In this file you register your hook
functions, indicate the parts of your plugin and the order of execution. (A
documentation of all available events to hook into can be found in chapter
<<Server-side hooks>>.)
[source,json]
----
{
"parts": [
{
"name": "nameThisPartHoweverYouWant",
"hooks": {
"authenticate": "ep_<plugin>/<file>:functionName1",
"expressCreateServer": "ep_<plugin>/<file>:functionName2"
},
"client_hooks": {
"acePopulateDOMLine": "ep_<plugin>/<file>:functionName3"
}
}
]
}
----
A hook function registration maps a hook name to a hook function specification.
The hook function specification looks like `ep_example/file.js:functionName`. It
consists of two parts separated by a colon: a module name to `require()` and the
name of a function exported by the named module. See
https://nodejs.org/docs/latest/api/modules.html#modules_module_exports[`module.exports`]
for how to export a function.
For the module name you can omit the `.js` suffix, and if the file is `index.js`
you can use just the directory name. You can also omit the module name entirely,
in which case it defaults to the plugin name (e.g., `ep_example`).
You can also omit the function name. If you do, Etherpad will look for an
exported function whose name matches the name of the hook (e.g.,
`authenticate`).
If either the module name or the function name is omitted (or both), the colon
may also be omitted unless the provided module name contains a colon. (So if the
module name is `C:\foo.js` then the hook function specification with the
function name omitted would be `"C:\\foo.js:"`.)
Examples: Suppose the plugin name is `ep_example`. All of the following are
equivalent, and will cause the `authorize` hook to call the `exports.authorize`
function in `index.js` from the `ep_example` plugin:
* `"authorize": "ep_example/index.js:authorize"`
* `"authorize": "ep_example/index.js:"`
* `"authorize": "ep_example/index.js"`
* `"authorize": "ep_example/index:authorize"`
* `"authorize": "ep_example/index:"`
* `"authorize": "ep_example/index"`
* `"authorize": "ep_example:authorize"`
* `"authorize": "ep_example:"`
* `"authorize": "ep_example"`
* `"authorize": ":authorize"`
* `"authorize": ":"`
* `"authorize": ""`
==== Client hooks and server hooks
There are server hooks, which will be executed on the server (e.g.
`expressCreateServer`), and there are client hooks, which are executed on the
client (e.g. `acePopulateDomLine`). Be sure to not make assumptions about the
environment your code is running in, e.g. don't try to access `process`, if you
know your code will be run on the client, and likewise, don't try to access
`window` on the server...
==== Styling
When you install a client-side plugin (e.g. one that implements at least one
client-side hook), the plugin name is added to the `class` attribute of the div
`#editorcontainerbox` in the main window. This gives you the opportunity of
tuning the appearance of the main UI in your plugin.
For example, this is the markup with no plugins installed:
[source,html]
----
<div id="editorcontainerbox" class="">
----
and this is the contents after installing `someplugin`:
[source,html]
----
<div id="editorcontainerbox" class="ep_someplugin">
----
This feature was introduced in Etherpad **1.8**.
==== Parts
As your plugins become more and more complex, you will find yourself in the need
to manage dependencies between plugins. E.g. you want the hooks of a certain
plugin to be executed before (or after) yours. You can also manage these
dependencies in your plugin definition file `ep.json`:
[source,json]
----
{
"parts": [
{
"name": "onepart",
"pre": [],
"post": ["ep_onemoreplugin/partone"],
"hooks": {
"storeBar": "ep_monospace/plugin:storeBar",
"getFoo": "ep_monospace/plugin:getFoo"
}
},
{
"name": "otherpart",
"pre": ["ep_my_example/somepart", "ep_otherplugin/main"],
"post": [],
"hooks": {
"someEvent": "ep_my_example/otherpart:someEvent",
"another": "ep_my_example/otherpart:another"
}
}
]
}
----
Usually a plugin will add only one functionality at a time, so it will probably
only use one `part` definition to register its hooks. However, sometimes you
have to put different (unrelated) functionalities into one plugin. For this you
will want use parts, so other plugins can depend on them.
===== pre/post
The `"pre"` and `"post"` definitions, affect the order in which parts of a
plugin are executed. This ensures that plugins and their hooks are executed in
the correct order.
`"pre"` lists parts that must be executed *before* the defining part. `"post"`
lists parts that must be executed *after* the defining part.
You can, on a basic level, think of this as double-ended dependency listing. If
you have a dependency on another plugin, you can make sure it loads before yours
by putting it in `"pre"`. If you are setting up things that might need to be
used by a plugin later, you can ensure proper order by putting it in `"post"`.
Note that it would be far more sane to use `"pre"` in almost any case, but if
you want to change config variables for another plugin, or maybe modify its
environment, `"post"` could definitely be useful.
Also, note that dependencies should *also* be listed in your package.json, so
they can be `npm install`'d automagically when your plugin gets installed.
=== Package definition
Your plugin must also contain a https://docs.npmjs.com/files/package.json[package definition
file], called package.json, in the
project root - this file contains various metadata relevant to your plugin, such
as the name and version number, author, project hompage, contributors, a short
description, etc. If you publish your plugin on npm, these metadata are used for
package search etc., but it's necessary for Etherpad plugins, even if you don't
publish your plugin.
[source,json]
----
{
"name": "ep_PLUGINNAME",
"version": "0.0.1",
"description": "DESCRIPTION",
"author": "USERNAME (REAL NAME) <MAIL@EXAMPLE.COM>",
"contributors": [],
"dependencies": {"MODULE": "0.3.20"},
"engines": {"node": ">=25.0.0"}
}
----
=== Templates
If your plugin adds or modifies the front end HTML (e.g. adding buttons or
changing their functions), you should put the necessary HTML code for such
operations in `templates/`, in files of type ".ejs", since Etherpad uses EJS for
HTML templating. See the following link for more information about EJS:
<https://github.com/visionmedia/ejs>.
=== Writing and running front-end tests for your plugin
Etherpad allows you to easily create front-end tests for plugins.
1. Create a new folder: `%your_plugin%/static/tests/frontend/specs`
2. Put your spec file in there. (Example spec files are visible in
`%etherpad_root_folder%/frontend/tests/specs`.)
3. Visit http://yourserver.com/frontend/tests and your front-end tests will run.