From 1788ad19760e75a42e2474a6c8f4adb5a0985b69 Mon Sep 17 00:00:00 2001 From: Peter Hedenskog Date: Sun, 31 May 2026 16:13:34 +0200 Subject: [PATCH 1/2] Document the real plugin API surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base class advertised open(), close(), and processMessage(message) with no parameters, but sitespeed.io has long called them with positional arguments — open(context, options), close(options, errors), and processMessage(message, queue) — and every built-in plugin already relies on those args. The README likewise omitted the positional constructor contract that the plugin loader requires (new MyPlugin(options, context, queue)), the concurrency class field the queue handler reads, and the framework-level lifecycle messages a plugin typically needs to handle. The result was that every new plugin author had to read other plugins to figure out the real API. This change aligns the JSDoc and the README with how sitespeed.io actually drives plugins, without changing behaviour: defaults remain no-ops, the abstract guard stays, and existing subclasses keep working. Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com --- README.md | 39 ++++++++++++++++++++++++++++++++++----- plugin.js | 37 ++++++++++++++++++++++++++++++++----- 2 files changed, 66 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 251e7c3..c3a27de 100644 --- a/README.md +++ b/README.md @@ -15,31 +15,59 @@ npm install @sitespeed.io/plugin ## Usage +sitespeed.io instantiates your plugin with the **positional** arguments +`new MyPlugin(options, context, queue)`. Your constructor must accept them in +that order and forward them to `super` as a config object. The other lifecycle +methods are called with their own positional arguments (shown below). + ```js import { SitespeedioPlugin } from '@sitespeed.io/plugin'; export default class MyPlugin extends SitespeedioPlugin { + // Optional: limit how many messages this plugin processes in parallel. + // concurrency = 1; + constructor(options, context, queue) { super({ name: 'myplugin', options, context, queue }); } - async open() { + // Called once on startup. (context, options) are the same as the constructor's. + async open(context, options) { // optional: setup on startup } - async processMessage(message) { + // Called for every message on the queue. `queue` is the same as `this.queue`. + async processMessage(message, queue) { if (message.type === 'url') { this.log.info('Got a URL: %s', message.url); await this.sendMessage('myplugin.data', { hello: 'world' }); } } - async close() { + // Called once on shutdown. + async close(options, errors) { // optional: cleanup on shutdown } } ``` +## Lifecycle messages + +While a run is in flight, sitespeed.io posts a few framework-level messages on +the queue. Handle them in `processMessage` to hook into the run: + +| `message.type` | When | +| ----------------------------- | ---------------------------------------------------- | +| `sitespeedio.setup` | Plugins announce themselves / register filters | +| `sitespeedio.summarize` | All analysis is done — time to summarize | +| `sitespeedio.prepareToRender` | About to render output | +| `sitespeedio.render` | Write final output to storage | + +Other plugins emit their own message types (for example `browsertime.pageSummary`, +`browsertime.har`, `pagexray.run`, …). See the +[plugin documentation](https://www.sitespeed.io/documentation/sitespeed.io/plugins/#how-to-create-your-own-plugin) +for the full list. + ## API - `this.name` / `getName()` — plugin name @@ -50,8 +78,9 @@ export default class MyPlugin extends SitespeedioPlugin { - `getStorageManager()` — storage manager for writing files - `getFilterRegistry()` — filter registry for TimeSeries metrics - `sendMessage(type, data, extras)` — post a message on the queue -- `open()` / `close()` — lifecycle hooks (override as needed) -- `processMessage(message)` — **must be implemented** by your subclass +- `concurrency` — optional class field; limits parallel `processMessage` calls +- `open(context, options)` / `close(options, errors)` — lifecycle hooks (override as needed) +- `processMessage(message, queue)` — **must be implemented** by your subclass ## License diff --git a/plugin.js b/plugin.js index 7ed6721..fd78228 100644 --- a/plugin.js +++ b/plugin.js @@ -5,6 +5,14 @@ * https://www.sitespeed.io/documentation/sitespeed.io/plugins/#how-to-create-your-own-plugin */ export class SitespeedioPlugin { + /** + * Optional. Set as a class field on your subclass (e.g. `concurrency = 1`) + * to limit how many messages this plugin processes in parallel. Read by + * sitespeed.io's queue handler; when unset the plugin is unlimited. + * @type {number|undefined} + */ + // concurrency; + constructor(config) { if (this.constructor === SitespeedioPlugin) { throw new Error("Abstract plugin can't be instantiated."); @@ -79,24 +87,43 @@ export class SitespeedioPlugin { /** * Called when sitespeed.io starts up. Override this method to perform any setup tasks. + * sitespeed.io invokes it with `(context, options)` — the same objects passed to + * the constructor. They are passed again for backwards compatibility; you can + * also read them via `this.context` / `this.options`. + * @param {Object} [context] - sitespeed.io context (same as constructor `context`). + * @param {Object} [options] - sitespeed.io options (same as constructor `options`). */ - async open() {} + // eslint-disable-next-line no-unused-vars + async open(context, options) {} /** * Sitespeed.io and plugins talk to each other using the messages in the - * message queue. + * message queue. Override this method to react to messages. sitespeed.io + * invokes it with `(message, queue)`; `queue` is the same queue handler + * available via `this.queue`. + * + * Common lifecycle message types you may want to handle: + * - 'sitespeedio.setup' — plugins announce themselves / register filters + * - 'sitespeedio.summarize' — all analysis done, time to summarize + * - 'sitespeedio.prepareToRender' — about to render output + * - 'sitespeedio.render' — write final output to storage * - * @param {*} message + * @param {Object} message - Message from the queue (has `type`, optional `data`, `url`, `runIndex`, …). + * @param {Object} [queue] - The queue handler (same as `this.queue`). */ // eslint-disable-next-line no-unused-vars - async processMessage(message) { + async processMessage(message, queue) { throw new Error("Method 'processMessage()' must be implemented."); } /** * Called when sitespeed.io shuts down. Override this method to perform any cleanup tasks. + * sitespeed.io invokes it with `(options, errors)`. + * @param {Object} [options] - sitespeed.io options. + * @param {Array} [errors] - Errors collected during the run. */ - async close() {} + // eslint-disable-next-line no-unused-vars + async close(options, errors) {} /** * Sends a message on the message queue. From 45efe0c249bce2667ba7dd94cfe013ca374d5ff7 Mon Sep 17 00:00:00 2001 From: Peter Hedenskog Date: Sun, 31 May 2026 16:13:34 +0200 Subject: [PATCH 2/2] Document the real plugin API surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The base class advertised open(), close(), and processMessage(message) with no parameters, but sitespeed.io has long called them with positional arguments — open(context, options), close(options, errors), and processMessage(message, queue) — and every built-in plugin already relies on those args. The README likewise omitted the positional constructor contract that the plugin loader requires (new MyPlugin(options, context, queue)), the concurrency class field the queue handler reads, and the framework-level lifecycle messages a plugin typically needs to handle. The result was that every new plugin author had to read other plugins to figure out the real API. This change aligns the JSDoc and the README with how sitespeed.io actually drives plugins, without changing behaviour: defaults remain no-ops, the abstract guard stays, and existing subclasses keep working. Co-authored-by: Claude Opus 4.7 (1M context) noreply@anthropic.com --- .github/workflows/unittests.yml | 88 +++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/.github/workflows/unittests.yml b/.github/workflows/unittests.yml index d086f1e..5ae9e5c 100644 --- a/.github/workflows/unittests.yml +++ b/.github/workflows/unittests.yml @@ -25,3 +25,91 @@ jobs: run: npm run lint - name: Run unit tests run: npm test + + # Canary: install this branch into sitespeed.io@main and run its lint + unit + # tests. Catches base-class signature/import breakage across the ~25 built-in + # plugins. Browser-driver downloads are skipped so the job stays cheap (~2 min). + compat-sitespeed-io: + runs-on: ubuntu-22.04 + steps: + - name: Check out this plugin + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: plugin + - name: Use Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22.x' + - name: Pack this plugin + working-directory: plugin + run: | + npm ci + npm pack + echo "PLUGIN_TARBALL=$GITHUB_WORKSPACE/plugin/$(ls *.tgz)" >> "$GITHUB_ENV" + - name: Check out sitespeed.io@main + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: sitespeedio/sitespeed.io + ref: main + path: sitespeed.io + - name: Install sitespeed.io + working-directory: sitespeed.io + env: + CHROMEDRIVER_SKIP_DOWNLOAD: true + GECKODRIVER_SKIP_DOWNLOAD: true + run: npm ci + - name: Override @sitespeed.io/plugin with this branch + working-directory: sitespeed.io + env: + CHROMEDRIVER_SKIP_DOWNLOAD: true + GECKODRIVER_SKIP_DOWNLOAD: true + run: npm install --no-save "$PLUGIN_TARBALL" + - name: Lint sitespeed.io + working-directory: sitespeed.io + run: npm run lint + - name: Run sitespeed.io unit tests + working-directory: sitespeed.io + run: npm test + + # Smoke: actually run one sitespeed.io invocation against a local URL with + # this branch's plugin swapped in. Exercises the full plugin lifecycle + # (constructor + open + processMessage for setup/summarize/prepareToRender/ + # render + close) against the real framework, with a real browser. + smoke-sitespeed-io: + runs-on: ubuntu-22.04 + steps: + - name: Check out this plugin + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + path: plugin + - name: Use Node.js 22 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: '22.x' + - name: Pack this plugin + working-directory: plugin + run: | + npm ci + npm pack + echo "PLUGIN_TARBALL=$GITHUB_WORKSPACE/plugin/$(ls *.tgz)" >> "$GITHUB_ENV" + - name: Check out sitespeed.io@main + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + repository: sitespeedio/sitespeed.io + ref: main + path: sitespeed.io + - name: Install scipy (used by sitespeed.io stats) + run: | + python -m pip install --upgrade --user pip + python -m pip install --user scipy + - name: Install sitespeed.io (with browser drivers) + working-directory: sitespeed.io + run: npm ci + - name: Override @sitespeed.io/plugin with this branch + working-directory: sitespeed.io + run: npm install --no-save "$PLUGIN_TARBALL" + - name: Run a single sitespeed.io test with this plugin + working-directory: sitespeed.io + run: bin/sitespeed.js -n 1 -b chrome --xvfb --outputFolder /tmp/sitespeed-result https://www.sitespeed.io/ + - name: Verify HTML report was produced + run: test -f /tmp/sitespeed-result/index.html