mirror of
https://github.com/ether/etherpad-lite.git
synced 2026-07-17 16:47:05 +00:00
* Roll Node.js floor back to >= 24 (Active LTS) Closes #7779. #7779 originally proposed bumping past the Node 25 stop-gap to Node 26. After re-checking the release schedule, the cleaner LTS target is actually Node 24: - Node 24 (Krypton) is currently in Active LTS, supported until ~May 2028. - Node 25 hit end-of-life on April 10 2026 — the floor merged in #7752 / #7749 / #7754 a day ago ships an already-EOL major. - Node 26 was released May 5 2026 and does not enter Active LTS until October 2026. So this PR reverts the Node 25 ratchet from those three PRs and lands on Node 24 — Etherpad's runtime floor stays on a supported LTS for the next ~2 years. Runtime / infra - `package.json` + `src/package.json`: `engines.node` `>=25.0.0` -> `>=24.0.0` - `bin/functions.sh`, `bin/installer.sh`, `bin/installer.ps1`: `REQUIRED_NODE_MAJOR` 25 -> 24 - `Dockerfile`: `node:25-alpine` -> `node:24-alpine` (both stages). Corepack-via-npm workaround is intentionally kept: it works on Node 24 (which still ships corepack) and on Node 25+ (which doesn't), so the same recipe survives the next LTS bump without churn. Comments reworded accordingly. - `snap/snapcraft.yaml`: pinned `NODE_VERSION` 25.9.0 -> 24.15.0; design notes + corepack comment adjusted - `packaging/nfpm.yaml`: `nodejs (>= 25)` -> `nodejs (>= 24)` in top-level depends + deb/rpm overrides - `packaging/bin/etherpad`: comment matches the new pin - `packaging/README.md`: build prereqs + apt install snippet point at `node_24.x`; the long-stale "engines.node floor is 20" line is fixed while we're here - `.github/workflows/*.yml`: setup-node `node-version` 25 -> 24 across every workflow; backend / frontend-admin / upgrade matrices `[25]` -> `[24]` - `.github/workflows/deb-package.yml`: `NODE_MAJOR=25` + `node_25.x` smoke-test installer -> 24 - `bin/plugins/lib/npmpublish.yml`: 25 -> 24 (template propagates to the ~80 ether/* plugins via update-plugins workflow) Docs - `README.md`: install one-liner + Requirements -> Node.js >= 24 - `doc/npm-trusted-publishing.md`: runner requirement -> Node 24 - `doc/plugins.md` / `doc/plugins.adoc`: plugin metadata example `engines.node` -> `">=24.0.0"` @types/node is left at ^25.8.0 — newer type definitions cover Node 24 runtime fine and avoid an unnecessary lockfile churn. Companion homepage one-liner change to follow on ether/ether.github.com. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs(plugins): example engines.node = ">=22.0.0", not core's floor Plugin code is overwhelmingly ace-hook glue and rarely uses Node-version- specific APIs, so plugin engines.node should reflect the plugin's own requirements, not track core. Showing core's 24-floor in the example encouraged plugin authors to blindly copy a tighter pin than necessary and locked plugins out of being installable on older Etherpad/Node deployments. Use the most-recent Node LTS that has actually reached EOL (20 -> EOL April 2026) as the example floor, i.e. >=22. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
319 lines
12 KiB
Markdown
319 lines
12 KiB
Markdown
# 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 `pnpm run plugins install ep_flubberworms` in Etherpad's root directory.
|
|
|
|
Also see [wiki article](https://github.com/ether/etherpad-lite/wiki/Available-Plugins) for more info.
|
|
|
|
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:
|
|
|
|
```
|
|
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
|
|
├ .github/workflows/ ◄─ CI workflows (backend / frontend tests, npm publish)
|
|
├ 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
|
|
[hooks](#all_hooks).)
|
|
|
|
```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
|
|
[`module.exports`](https://nodejs.org/docs/latest/api/modules.html#modules_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:
|
|
|
|
```html
|
|
<div id="editorcontainerbox" class="">
|
|
```
|
|
|
|
and this is the contents after installing `someplugin`:
|
|
|
|
```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`:
|
|
|
|
```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 [package definition
|
|
file](https://docs.npmjs.com/files/package.json), 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.
|
|
|
|
```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": ">=22.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>.
|
|
|
|
## Plugin-namespaced pad-wide options
|
|
|
|
Plugins can ride the existing `padoptions` COLLABROOM rail to store
|
|
pad-wide settings — broadcast to every connected client, persisted with the
|
|
pad, and honored by `enforceSettings` — instead of inventing their own
|
|
message type and storage. The model matches how `enablePadWideSettings`
|
|
works for native toggles like sticky chat or line numbers.
|
|
|
|
### Capability detection
|
|
|
|
```js
|
|
let padOptionsPluginPassthrough = false;
|
|
try {
|
|
// The require throws on Etherpad versions that predate this capability;
|
|
// plugins should degrade gracefully (typically falling back to a per-user
|
|
// cookie toggle) when the flag is missing.
|
|
padOptionsPluginPassthrough =
|
|
require('ep_etherpad-lite/node/utils/PluginCapabilities')
|
|
.padOptionsPluginPassthrough === true;
|
|
} catch (_e) { /* older core */ }
|
|
```
|
|
|
|
The flag means the core has the passthrough patch *available*. Whether it
|
|
is actually *enabled* at runtime is a separate per-instance setting — see
|
|
below.
|
|
|
|
### Runtime flag
|
|
|
|
The passthrough is gated by `settings.enablePluginPadOptions`, default
|
|
`false`. Operators must opt in via `settings.json`:
|
|
|
|
```json
|
|
{
|
|
"enablePluginPadOptions": true
|
|
}
|
|
```
|
|
|
|
When enabled, the server reflects the value to every client via
|
|
`clientVars.enablePluginPadOptions` so plugins can detect both *capable*
|
|
(static) and *active* (per-pad request) at the same point.
|
|
|
|
### Key namespace
|
|
|
|
Plugins must use keys matching `/^ep_[a-z0-9_]+$/`. The recommended pattern
|
|
is `ep_<plugin_name>` (e.g. `ep_table_of_contents`); compose multiple
|
|
pad-wide settings under one key as a plain object:
|
|
|
|
```js
|
|
pad.changePadOption('ep_my_plugin', {enabled: true, depth: 3});
|
|
```
|
|
|
|
The server passes through any matching key on the existing `padoptions`
|
|
message, persists it with the pad, and broadcasts it to every connected
|
|
client. `pad.padOptions.ep_my_plugin` reflects the latest value on every
|
|
client.
|
|
|
|
### Validation
|
|
|
|
Server-side `Pad.normalizePadSettings()` enforces three rules on every
|
|
plugin-namespaced key:
|
|
|
|
- Values must round-trip through `JSON.stringify` (no functions, symbols,
|
|
BigInt, or circular references).
|
|
- Each key's serialized payload must fit within **64 KB**.
|
|
- The combined size of all `ep_*` values per pad must fit within **256 KB**.
|
|
|
|
Values that fail any of these rules are dropped with a `console.warn`; the
|
|
rest of the settings round-trip cleanly. The caps prevent a misbehaving
|
|
plugin from bloating the persisted pad payload or the COLLABROOM
|
|
broadcast.
|
|
|
|
## 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.
|