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 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.