From 98a89a33911b7243a6ea6ec80703f6b8abd11378 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Fri, 5 Dec 2025 06:02:51 +0100 Subject: [PATCH 01/14] change v headline --- CHANGELOG.md | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03e7702..cd490fd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,11 @@ # Changelog -## [25.0.1] - 2025-11-14 +## v25.0.1 - 2025-11-14 - provenance release to get green badge next to npm package version - bump all provenance updated packages -## [25.0.0] - 2025-03-17 +## v25.0.0 - 2025-03-17 It has been possible to recover a running engine. The execution was overwritten and all references to timers etc was lost. This stops with this version. Either stop the the execution or wait for it to end if the engine should be re-used. It is highly recommended to initiate a new Engine when recovering from state. @@ -17,44 +17,44 @@ It has been possible to recover a running engine. The execution was overwritten - minor update of moddle-context-serializer brings some performance improvement -## [24.0.1] - 2025-03-14 +## v24.0.1 - 2025-03-14 - fix recover engine with options not keeping scripts and logger from when initiated > NB! Next major versions will not accept recovering a running engine. -## [24.0.0] - 2025-02-08 +## v24.0.0 - 2025-02-08 - major update [`bpmn-elements@17`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md), something about mitigating weird formatting behaviour -## [23.0.2] - 2024-10-22 +## v23.0.2 - 2024-10-22 - move `smqp` to peerDependencies since it's included in `bpmn-elements` - patch [`bpmn-elements@16.2.1`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) - use optional chaining and remove futile object creations -## [23.0.1] - 2024-09-08 +## v23.0.1 - 2024-09-08 - patch [`bpmn-elements@16.1.0`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) - fix AppVeyor build badge -## [23.0.0] - 2024-08-08 +## v23.0.0 - 2024-08-08 ### Breaking - changed `ConditionalEventDefinition` behaviour in [`bpmn-elements@16`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) -## [22.0.2] - 2024-07-10 +## v22.0.2 - 2024-07-10 - patch [`bpmn-elements@15.0.3`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) -## [22.0.1] - 2024-06-19 +## v22.0.1 - 2024-06-19 - for a number of years the `extendFn` option has been accepted, but never passed to moddle-context-serializer. Now it is - fix type declaration for options `extendFn` and `TypeResolver` - type declare that a generic listener instance with an emit function works just as well as a full blown new EventEmitter -## [22.0.0] - 2024-06-14 +## v22.0.0 - 2024-06-14 Performance tweaks. @@ -66,11 +66,11 @@ Performance tweaks. - drop support for node 14 and 16 - JavaScripts.Script property scripts is now a Map -## [21.1.0] - 2024-05-17 +## v21.1.0 - 2024-05-17 - minor bump [`bpmn-elements@14.1.0`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md), addresses issue #180 -## [21.0.0] - 2024-05-04 +## v21.0.0 - 2024-05-04 Stop execution if invalid time duration, cycle, or date is encountered. @@ -78,36 +78,36 @@ Stop execution if invalid time duration, cycle, or date is encountered. - invalid `TimerEventDefinition` timer type value stops execution according to [`bpmn-elements@14`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md). Old behaviour can be achieved by using bpmn-elements@13 -## 20.0.2 +## v20.0.2 - patch away package prettier from smqp -## 20.0.1 +## v20.0.1 - hoist current definition environment output to engine environment output if run fails - Major bump `bpmn-moddle@9` - Minor bump [`moddle-context-serializer@4.2`](https://github.com/paed01/moddle-context-serializer/blob/master/CHANGELOG.md) - Minor bump [`bpmn-elements@13.2.0`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) -## 20.0.0 +## v20.0.0 - turn into esm with exports for node - build with node >= 18, should still work with earlier versions but proceed with caution and make tests - remove eslint formatting rules in favor of prettier, touched basically all files but now it is "pretty" - prototype `ProcessOutputDataObject` and make properties id and type readonly -## 19.0.1 +## v19.0.1 - Patch [`bpmn-elements`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) -## 19.0.0 +## v19.0.0 Upgrade is recommended since nasty evergroving state size is fixed. - Major bump [`bpmn-elements@13`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) - remove enumerable flag on prototype properties -## 18.0.0 +## v18.0.0 Only breaking if multi-instance sub-process executions are inspected after sub-process run is completed. Picture a multi-instance sequential sub-process with a cardinality of 100. One hundred items in a list occupies some memory. That will not stand. Consequently, they are now removed when iteration completes and eventually collected by gc. From 85c00033fd4eedd14bfdd3f39cb5518ca69319e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 13 Jun 2026 08:44:15 +0200 Subject: [PATCH 02/14] upgrade bpmn-elements@18 --- .github/workflows/build.yaml | 4 +- .github/workflows/release.yaml | 2 +- .gitignore | 4 + .nvmrc | 2 +- AGENTS.md | 65 +++++++ CHANGELOG.md | 12 ++ docs/API.md | 17 +- docs/Examples.md | 20 +- eslint.config.js | 13 +- lib/index.cjs | 99 +++++++++- package.json | 27 +-- scripts/generate-api-toc.js | 126 ++++++++++--- src/index.js | 84 ++++++++- test/feature/extend-feature.js | 6 +- test/feature/issues-feature.js | 72 ++++--- test/helpers/testHelpers.js | 4 +- test/issues-test.js | 17 +- tsconfig.json | 24 +-- types/bpmn-engine.d.ts | 334 --------------------------------- types/index.d.ts | 153 +++++++++++++++ types/interfaces.d.ts | 80 ++++++++ 21 files changed, 697 insertions(+), 468 deletions(-) create mode 100644 AGENTS.md delete mode 100644 types/bpmn-engine.d.ts create mode 100644 types/index.d.ts create mode 100644 types/interfaces.d.ts diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 04dc54c..6a4651f 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - node-version: [18, 20, 22, 24, latest] + node-version: [20, 22, 24, latest] steps: - name: Checkout - uses: actions/checkout@v5 + uses: actions/checkout@v6 - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v6 with: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index a9dc6c5..f42f20b 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,7 +13,7 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@v6 - uses: actions/setup-node@v6 with: diff --git a/.gitignore b/.gitignore index 4e2e76a..a58ea97 100644 --- a/.gitignore +++ b/.gitignore @@ -36,3 +36,7 @@ config/_revision package-lock.json *.log + +# Claude Code +CLAUDE.md +.claude diff --git a/.nvmrc b/.nvmrc index 3c03207..209e3ef 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -18 +20 diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..acb241e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,65 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Commands + +`.nvmrc` pins Node 18; `engines.node` is `>=18`. The test reporter `@bonniernews/hot-bev` and `mocha-cakes-2` UI handle parallel execution and Gherkin-style Features. + +- `npm test` — runs `mocha -p` with the hot-bev reporter (parallel). `posttest` then runs `lint`, `toc` (regenerates API doc TOCs), and `dist`. +- `npm run wintest` — same suite, plain `mocha` (no `-p`/parallel; for environments where worker pools misbehave). +- `npm run lint` — eslint (cached) + prettier check. +- `npm run cov:html` / `npm run test:lcov` — coverage variants via c8. +- `npm run toc` — regenerates the table of contents in `docs/API.md` and `docs/Examples.md` via the in-repo `scripts/generate-api-toc.js` (no `markdown-toc` dep). It takes the markdown files as a comma- or space-separated argument, rewrites the block between `` and ``, skips the first H1 (the doc title — `docs/API.md` keeps a plain `# API Reference` title for this), and disambiguates duplicate anchors GitHub-style (`#getstate`, `#getstate-1`). +- `npm run test-md` — runs `texample` against `docs/API.md`, `docs/Examples.md`, `docs/Upgrade.md`. **Not** in the regular `posttest` chain; run manually when API doc examples change. +- `npm run dist` — rollup builds `src/index.js` → `lib/index.cjs`. The footer trick `module.exports = Object.assign(exports.default, exports)` is what lets the CJS bundle's `require('bpmn-engine')` return the `Engine` constructor _and_ expose named exports — don't remove it. +- Single test file: `npx mocha test/Engine-test.js`. Single test: append `--grep ""`. Default mocha timeout is 1000ms (see `.mocharc.json`); feature tests under `test/feature/` use the `mocha-cakes-2` BDD UI (`Feature`/`Scenario`/`Given`/`When`/`Then`). + +## Architecture + +The `Engine` is a thin orchestrator on top of four heavyweights — every file in `src/` is glue: + +``` +bpmn-moddle (parse XML) + │ + ▼ +moddle-context-serializer ──► persistable JSON state (serialize / deserialize) + │ + ▼ +bpmn-elements (behaviour functions: Process, Task, *EventDefinition, …) + │ + ▼ +smqp (peer dep) ── message broker driving execution +``` + +`src/index.js` (~630 lines) wires these together: + +1. **Construction** — `Engine(options)` builds a `TypeResolver` from `bpmn-elements` + user `elements` overrides, creates an `Elements.Environment`, and instantiates a `smqp` `Broker` for engine-level events. Defaults can be passed via `Logger`, `scripts` (defaults to `JavaScripts.js` — Node `vm`-based inline script handler), `extensions`, etc. +2. **Source loading** — `addSource({ source | sourceContext })` queues sources. `source` is raw BPMN XML; `sourceContext` is a serializer output (skips XML parse). Both end up as `moddle-context-serializer` `SerializableContext`s. +3. **Execution** — `execute()` resolves all pending sources to `Definition`s (from `bpmn-elements`), starts them via the broker, and listens to broker events to surface activity status (`executing` / `timer` / `wait` / `idle`). +4. **State** — `getState()` walks each definition and pulls its serializable state; `recover(state)` reconstructs definitions; `resume()` restarts execution. As of v25, **a running engine cannot be recovered** — must be stopped first or use a fresh `Engine` instance (see CHANGELOG). + +### Conventions worth knowing + +- **Symbol-keyed private state.** All instance internals on `Engine`/execution use `Symbol.for('engine')`, `Symbol.for('environment')`, etc. This is the project's privacy convention — don't replace with `#fields` (would break `Object.assign`-based extension and the recover/resume state walking). +- **`getOptionsAndCallback.js`** — small helper that lets every public method accept either `(options, callback)` or `(callback)` style. Preserves the legacy callback-friendly API while the implementation returns Promises. +- **Extensions live under `src/extensions/`.** `ProcessOutputDataObject.js` is the canonical example — extensions plug into `bpmn-elements` via the engine's `extensions` option and are typically called for specific element types. +- **Test fixtures under `test/resources/`** are real `.bpmn` XML files plus a few `.json` extensions. `test/resources/JsExtension.js` and `test/resources/js-bpmn-moddle.json` define a custom moddle extension namespace used across multiple feature tests. +- **`mocha-cakes-2` ≠ standard mocha.** Files under `test/feature/` use Gherkin-flavored globals (`Feature`, `Scenario`, `Given`, `When`, `Then`, `And`, `But`). They're ESLint-allowed via the eslint config's globals. + +## Build & publishing + +- `type: module` in `package.json`; ESM is `src/index.js`, CJS is `lib/index.cjs` (rollup), types are hand-maintained at `types/index.d.ts` (the published `declare module 'bpmn-engine'` surface) with shared interface definitions in `types/interfaces.d.ts` (referenced by `src` JSDoc via the tsconfig `types` path). +- `prepack` runs `dist`, so `npm publish` always ships a fresh CJS bundle. `lib/index.cjs` is tracked in git — regenerate (`npm run dist`) and commit alongside source changes that affect the public API. +- Tests import via package self-reference (`import { Engine } from 'bpmn-engine'`), not `'../src/index.js'`. This exercises the same module resolution consumers use. + +## Dependency notes + +- **`moddle-context-serializer`** — this engine is the primary consumer that calls `.serialize()` / `deserialize()` for state persistence. The serialized JSON is a _runtime persistence format_ for live engines, so changes to it break upgrade-path for deployed engines, not just code. Keep that in mind when bumping the dep — even a "harmless" field rename in the serializer's output ripples here. +- **`bpmn-elements`** — supplies the behaviour function registry passed to `TypeResolver`. Adding new element types means adding an entry there first. +- **`smqp`** is a `peerDependency` (`>=9`) — same-maintainer message broker that drives both engine-level and definition-level event flow. Consumers install it directly. Coordinated breaking changes are feasible since the same hand maintains it. +- **`bpmn-moddle`** is a regular dep (currently `^9.0.4`). v10 changed default → named export but `src/index.js` still does `import BpmnModdle from 'bpmn-moddle'` — bumping to v10 will break the import. + +## Downstream consumers + +This engine is the user-facing entry point of the BPMN library stack — most application code that runs BPMN processes uses `bpmn-engine` directly, not the lower-level `bpmn-elements` / `moddle-context-serializer`. State persistence shape (the JSON returned by `getState()`) is therefore the most consumer-sensitive surface — schema changes break stored process state on upgrade. diff --git a/CHANGELOG.md b/CHANGELOG.md index cd490fd..02a152f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,17 @@ # Changelog +## v26.0.0 - 2026-06-13 + +### Breaking + +- major update [`bpmn-elements@18`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) — non-selected gateway flows are no longer discarded +- update to [`moddle-context-serializer@6`](https://github.com/paed01/moddle-context-serializer/blob/master/CHANGELOG.md) +- require `smqp@>=13` peer dependency + +### Other + +- ship hand-maintained TypeScript declarations + ## v25.0.1 - 2025-11-14 - provenance release to get green badge next to npm package version diff --git a/docs/API.md b/docs/API.md index 68e3d08..490537d 100644 --- a/docs/API.md +++ b/docs/API.md @@ -1,8 +1,4 @@ - - -# 25.0.1 API Reference - - +# API Reference @@ -21,7 +17,7 @@ - [`resume([options, [callback]])`](#resumeoptions-callback) - [Execution API](#execution-api) - [`getActivityById(activityId)`](#getactivitybyidactivityid) - - [`getState()`](#getstate) + - [`getState()`](#getstate-1) - [`signal(message[, options])`](#signalmessage-options) - [`cancelActivity(message)`](#cancelactivitymessage) - [Engine events](#engine-events) @@ -319,7 +315,7 @@ import { EventEmitter } from 'node:events'; import BpmnModdle from 'bpmn-moddle'; import * as elements from 'bpmn-elements'; import { Engine } from 'bpmn-engine'; -import Serializer, { TypeResolver } from 'moddle-context-serializer'; +import { Serializer, TypeResolver } from 'moddle-context-serializer'; const engine = new Engine({ name: 'add source', @@ -413,11 +409,14 @@ Asynchronous function to get state of a running execution. The saved state will include the following content: -- `state`: `running` or `idle` +- `name`: engine name +- `state`: `running`, `idle`, `stopped`, or `error` +- `stopped`: boolean stopped - `engineVersion`: module package version -- `moddleOptions`: Engine moddleOptions +- `environment`: serialized engine environment - `definitions`: List of definitions - `state`: State of definition, `pending`, `running`, or `completed` + - `source`: serialized definition source - `processes`: Object with processes with id as key - `variables`: Execution variables - `services`: Execution services diff --git a/docs/Examples.md b/docs/Examples.md index 0912045..2bbbbce 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -765,7 +765,7 @@ import { fileURLToPath } from 'node:url'; import { Engine } from 'bpmn-engine'; import BpmnModdle from 'bpmn-moddle'; import * as elements from 'bpmn-elements'; -import Serializer, { TypeResolver } from 'moddle-context-serializer'; +import { Serializer, TypeResolver } from 'moddle-context-serializer'; const camunda = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json'); @@ -828,23 +828,23 @@ In this example the state of the execution is published on a message broker. Sub ```js import { randomUUID } from 'node:crypto'; import { createRequire } from 'node:module'; -import { fileURLToPath } = from 'node:url'; +import { fileURLToPath } from 'node:url'; +import { EventEmitter } from 'node:events'; import { Engine } from 'bpmn-engine'; -import { EventEmitter } from 'ndoe:events'; import { publish } from './dbbroker.js'; import { getSourceSync, getAllowedServices, getExtensions } from './utils.js'; -const camundaModdle = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json') +const camunda = createRequire(fileURLToPath(import.meta.url))('camunda-bpmn-moddle/resources/camunda.json'); function ignite(executionId, options = {}) { const { name, settings } = options; const listener = new EventEmitter(); listener.on('activity.wait', (_, execution) => { - return publishEvent('bpmn.state.update', {state: execution.getState()}); + return publishEvent('bpmn.state.update', { state: execution.getState() }); }); listener.on('activity.end', (_, execution) => { - return publishEvent('bpmn.state.update', {state: execution.getState()}); + return publishEvent('bpmn.state.update', { state: execution.getState() }); }); listener.on('activity.timer', (api, execution) => { return publishEvent('bpmn.state.expires', { @@ -859,7 +859,7 @@ function ignite(executionId, options = {}) { }); }); - const engine = BpmnEngine({ + const engine = new Engine({ moddleOptions: { camunda, }, @@ -868,13 +868,13 @@ function ignite(executionId, options = {}) { ...settings, executionId, enableDummyService: false, - } + }, }); engine.once('end', () => { publishEvent('bpmn.completed'); }); engine.once('error', (err) => { - publishEvent('bpmn.error', {message: err.message, error: err}); + publishEvent('bpmn.error', { message: err.message, error: err }); }); return { engine, listener }; @@ -888,7 +888,7 @@ function ignite(executionId, options = {}) { } } -const {engine} = ignite(randomUUID(), { +const { engine } = ignite(randomUUID(), { name: 'persisted engine #1', source: getSourceSync('./mother-of-all.bpmn'), services: getAllowedServices(), diff --git a/eslint.config.js b/eslint.config.js index 9ce23c5..57280c9 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -74,7 +74,7 @@ export default [ languageOptions: { parserOptions: { sourceType: 'module', - ecmaVersion: 2020, + ecmaVersion: 2025, }, }, rules, @@ -87,6 +87,17 @@ export default [ }, }, }, + { + files: ['scripts/**/*.js'], + languageOptions: { + globals: { + ...globals.node, + }, + }, + rules: { + 'no-process-exit': 0, + }, + }, { files: ['test/**/*.js'], languageOptions: { diff --git a/lib/index.cjs b/lib/index.cjs index f6e49f4..73d756f 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -8,7 +8,7 @@ var node_url = require('node:url'); var BpmnModdle = require('bpmn-moddle'); var Elements = require('bpmn-elements'); var smqp = require('smqp'); -var serializer = require('moddle-context-serializer'); +var moddleContextSerializer = require('moddle-context-serializer'); var Debug = require('debug'); var node_vm = require('node:vm'); @@ -161,6 +161,17 @@ ProcessOutputDataObject.prototype.write = function writeDataObject(broker, excha return broker.publish(exchange, `${routingKeyPrefix}response`, { id, name, type, value }, messageProperties); }; +/** + * @typedef {import('types').BpmnEngineOptions} BpmnEngineOptions + * @typedef {import('types').BpmnEngineExecuteOptions} BpmnEngineExecuteOptions + * @typedef {import('types').BpmnEngineExecutionState} BpmnEngineExecutionState + * @typedef {import('types').BpmnEngineEvent} BpmnEngineEvent + * @typedef {import('types').BpmnEngineRunningStatus} BpmnEngineRunningStatus + * @typedef {import('types').BpmnMessage} BpmnMessage + * @typedef {import('types').SerializableContext} SerializableContext + */ + + const nodeRequire = node_module.createRequire(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))); const { version: engineVersion } = nodeRequire('../package.json'); @@ -175,6 +186,10 @@ const kState = Symbol.for('state'); const kStopped = Symbol.for('stopped'); const kTypeResolver = Symbol.for('type resolver'); +/** + * BPMN 2.0 execution engine. + * @param {import('types').BpmnEngineOptions} [options] + */ function Engine(options) { if (!(this instanceof Engine)) return new Engine(options); @@ -188,7 +203,7 @@ function Engine(options) { this.logger = opts.Logger('engine'); - this[kTypeResolver] = serializer.TypeResolver( + this[kTypeResolver] = moddleContextSerializer.TypeResolver( { ...Elements__namespace, ...opts.elements, @@ -260,6 +275,12 @@ Object.defineProperties(Engine.prototype, { }, }); +/** + * Execute the loaded definitions. + * @param {BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {(err: Error, execution?: Execution) => void} [callback] + * @returns {Promise} + */ Engine.prototype.execute = async function execute(...args) { const [executeOptions, callback] = getOptionsAndCallback(...args); try { @@ -273,12 +294,19 @@ Engine.prototype.execute = async function execute(...args) { return execution._execute(executeOptions, callback); }; +/** @returns {Promise} */ Engine.prototype.stop = function stop() { const execution = this.execution; if (!execution) return; return execution.stop(); }; +/** + * Recover engine from saved state. + * @param {BpmnEngineExecutionState | null} savedState + * @param {BpmnEngineOptions} [recoverOptions] + * @returns {Engine} + */ Engine.prototype.recover = function recover(savedState, recoverOptions) { if (this[kExecution]?.isRunning) { throw new Error('cannot recover a running engine'); @@ -306,7 +334,7 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { const typeResolver = this[kTypeResolver]; const loadedDefinitions = (this[kLoadedDefinitions] = savedState.definitions.map((dState) => { let source; - if (dState.source) source = serializer.deserialize(JSON.parse(dState.source), typeResolver); + if (dState.source) source = moddleContextSerializer.deserialize(JSON.parse(dState.source), typeResolver); else source = preSources.find((s) => s.id === dState.id); pendingSources.add(source); @@ -324,6 +352,12 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { return this; }; +/** + * Resume execution from a previously recovered state. + * @param {BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {(err: Error, execution?: Execution) => void} [callback] + * @returns {Promise} + */ Engine.prototype.resume = async function resume(...args) { const [resumeOptions, callback] = getOptionsAndCallback(...args); @@ -345,6 +379,10 @@ Engine.prototype.resume = async function resume(...args) { return execution._resume(resumeOptions, callback); }; +/** + * Add a pre-serialized source context to the engine. + * @param {{ sourceContext: SerializableContext }} [options] + */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; const loadedDefinitions = this[kLoadedDefinitions]; @@ -352,16 +390,25 @@ Engine.prototype.addSource = function addSource(options) { this[kPendingSources].add(options.sourceContext); }; +/** + * @param {BpmnEngineExecuteOptions} [executeOptions] + * @returns {Promise} + */ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { const loadedDefinitions = this[kLoadedDefinitions]; if (loadedDefinitions?.length) return Promise.resolve(loadedDefinitions); return this._loadDefinitions(executeOptions); }; +/** + * @param {string} id + * @returns {Promise} + */ Engine.prototype.getDefinitionById = async function getDefinitionById(id) { return (await this.getDefinitions()).find((d) => d.id === id); }; +/** @returns {Promise} */ Engine.prototype.getState = async function getState() { const execution = this.execution; if (execution) return execution.getState(); @@ -370,6 +417,11 @@ Engine.prototype.getState = async function getState() { return new Execution(this, definitions, this.options).getState(); }; +/** + * @template R + * @param {BpmnEngineEvent} eventName + * @returns {Promise} + */ Engine.prototype.waitFor = function waitFor(eventName) { const self = this; return new Promise((resolve, reject) => { @@ -387,12 +439,14 @@ Engine.prototype.waitFor = function waitFor(eventName) { }); }; +/** @internal */ Engine.prototype._loadDefinitions = async function loadDefinitions(executeOptions) { const runSources = await Promise.all(this[kPendingSources]); const loadedDefinitions = (this[kLoadedDefinitions] = runSources.map((source) => this._loadDefinition(source, executeOptions))); return loadedDefinitions; }; +/** @internal */ Engine.prototype._loadDefinition = function loadDefinition(serializedContext, executeOptions) { const environment = this.environment; const context = new Elements__namespace.Context( @@ -415,20 +469,29 @@ Engine.prototype._loadDefinition = function loadDefinition(serializedContext, ex return new Elements__namespace.Definition(context); }; +/** @internal */ Engine.prototype._serializeSource = async function serializeSource(source) { const moddleContext = await this._getModdleContext(source); return this._serializeModdleContext(moddleContext); }; +/** @internal */ Engine.prototype._serializeModdleContext = function serializeModdleContext(moddleContext) { - return serializer(moddleContext, this[kTypeResolver], this.options.extendFn); + return moddleContextSerializer.Serializer(moddleContext, this[kTypeResolver], this.options.extendFn); }; +/** @internal */ Engine.prototype._getModdleContext = function getModdleContext(source) { const bpmnModdle = new BpmnModdle(this.options.moddleOptions); return bpmnModdle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); }; +/** + * @param {Engine} engine + * @param {any[]} definitions + * @param {BpmnEngineExecuteOptions} [options] + * @param {boolean} [isRecovered] + */ function Execution(engine, definitions, options, isRecovered = false) { this.name = engine.name; this.options = options; @@ -478,6 +541,7 @@ Object.defineProperties(Execution.prototype, { }, }); +/** @internal */ Execution.prototype._execute = function execute(executeOptions, callback) { this._setup(executeOptions); this[kStopped] = false; @@ -499,6 +563,7 @@ Execution.prototype._execute = function execute(executeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._resume = function resume(resumeOptions, callback) { this._setup(resumeOptions); @@ -512,6 +577,7 @@ Execution.prototype._resume = function resume(resumeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callback) { if (!callback) return; @@ -562,6 +628,7 @@ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callba } }; +/** @returns {Promise} */ Execution.prototype.stop = async function stop() { const engine = this[kEngine]; const prom = engine.waitFor('stop'); @@ -579,6 +646,7 @@ Execution.prototype.stop = async function stop() { return result; }; +/** @internal */ Execution.prototype._setup = function setup(setupOptions) { const listener = setupOptions?.listener || this.options.listener; if (listener && typeof listener.emit !== 'function') throw new Error('listener.emit is not a function'); @@ -598,6 +666,7 @@ Execution.prototype._setup = function setup(setupOptions) { } }; +/** @internal */ Execution.prototype._onChildMessage = function onChildMessage(routingKey, message, owner) { const { environment: ownerEnvironment } = owner; const listener = ownerEnvironment.options?.listener; @@ -667,6 +736,7 @@ Execution.prototype._onChildMessage = function onChildMessage(routingKey, messag } }; +/** @internal */ Execution.prototype._complete = function complete(eventType, content, messageProperties) { const timers = this.environment.timers; timers.executing.slice().forEach((ref) => timers.clearTimeout(ref)); @@ -674,11 +744,13 @@ Execution.prototype._complete = function complete(eventType, content, messagePro return eventType !== 'error' && this[kEngine].emit(eventType, this); }; +/** @internal */ Execution.prototype._teardownDefinition = function teardownDefinition(definition) { this[kExecuting].delete(definition); definition.broker.cancel('_engine_definition'); }; +/** @internal */ Execution.prototype._saveOutput = function saveOutput(output) { if (!output || typeof output !== 'object') return; @@ -694,6 +766,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; +/** @returns {BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -713,6 +786,10 @@ Execution.prototype.getState = function getState() { }; }; +/** + * @param {string} activityId + * @returns {any} + */ Execution.prototype.getActivityById = function getActivityById(activityId) { for (const definition of this.definitions) { const activity = definition.getActivityById(activityId); @@ -720,6 +797,7 @@ Execution.prototype.getActivityById = function getActivityById(activityId) { } }; +/** @returns {any[]} */ Execution.prototype.getPostponed = function getPostponed() { const definitions = this.stopped ? this.definitions : this[kExecuting]; let result = []; @@ -729,6 +807,10 @@ Execution.prototype.getPostponed = function getPostponed() { return result; }; +/** + * @param {BpmnMessage} [payload] + * @param {{ ignoreSameDefinition?: boolean }} [signalOptions] + */ Execution.prototype.signal = function signal(payload, signalOptions) { for (const definition of this[kExecuting]) { if (signalOptions?.ignoreSameDefinition && payload?.parent?.id === definition.id) continue; @@ -736,26 +818,35 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; +/** @param {BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); } }; +/** + * @template T + * @param {BpmnEngineEvent} eventName + * @returns {Promise} + */ Execution.prototype.waitFor = function waitFor(...args) { return this[kEngine].waitFor(...args); }; +/** @internal */ Execution.prototype._onBrokerReturn = function onBrokerReturn(message) { if (message.properties.type === 'error') { this[kEngine].emit('error', message.content); } }; +/** @internal */ Execution.prototype._debug = function debug(msg) { this[kEngine].logger.debug(`<${this.name}> ${msg}`); }; +/** @internal */ Execution.prototype._getActivityStatus = function getActivityStatus() { let status = 'idle'; const executing = this[kExecuting]; diff --git a/package.json b/package.json index dcb229f..fc601ad 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { "name": "bpmn-engine", "description": "BPMN 2.0 execution engine. Open source javascript workflow engine.", - "version": "25.0.1", + "version": "26.0.0", "type": "module", "module": "./src/index.js", "main": "./lib/index.cjs", - "types": "./types/bpmn-engine.d.ts", + "types": "./types/index.d.ts", "sideEffects": false, "exports": { ".": { - "types": "./types/bpmn-engine.d.ts", + "types": "./types/index.d.ts", "require": "./lib/index.cjs", "import": "./src/index.js" } @@ -59,29 +59,30 @@ ], "devDependencies": { "@bonniernews/hot-bev": "^0.4.0", + "@eslint/js": "^10.0.1", "@rollup/plugin-commonjs": "^29.0.0", - "@types/bpmn-moddle": "^5.1.6", - "@types/node": "^18.19.31", + "@types/bpmn-moddle": "^10.0.0", + "@types/node": "^20.19.40", "bent": "^7.3.12", - "c8": "^10.0.0", + "c8": "^11.0.0", "camunda-bpmn-moddle": "^7.0.1", "chai": "^6.2.0", - "chronokinesis": "^7.0.1", - "eslint": "^9.10.0", - "markdown-toc": "^1.2.0", + "chronokinesis": "^8.0.0", + "eslint": "^10.5.0", + "globals": "^17.6.0", "mocha": "^11.0.1", "mocha-cakes-2": "^3.3.0", "prettier": "^3.2.5", "rollup": "^4.10.0", - "texample": "^0.1.0" + "texample": "^1.0.2" }, "dependencies": { - "bpmn-elements": "^17.2.2", + "bpmn-elements": "^18.0.0", "bpmn-moddle": "^9.0.4", "debug": "^4.4.3", - "moddle-context-serializer": "^4.4.1" + "moddle-context-serializer": "^6.0.0" }, "peerDependencies": { - "smqp": ">=9" + "smqp": "^13.0.1" } } diff --git a/scripts/generate-api-toc.js b/scripts/generate-api-toc.js index 108cb3b..daf4666 100644 --- a/scripts/generate-api-toc.js +++ b/scripts/generate-api-toc.js @@ -1,36 +1,106 @@ -import fs from 'node:fs'; -import { createRequire } from 'node:module'; +#!/usr/bin/env node +/* eslint-disable no-console */ +import { readFile, writeFile } from 'node:fs/promises'; import { fileURLToPath } from 'node:url'; -import Toc from 'markdown-toc'; +import { dirname, resolve } from 'node:path'; -const nodeRequire = createRequire(fileURLToPath(import.meta.url)); -const { version } = nodeRequire('../package.json'); +const TOC_START = ''; +const TOC_END = ''; -const filenames = getFileNames(); +const here = dirname(fileURLToPath(import.meta.url)); +const repoRoot = resolve(here, '..'); -function getFileNames() { - const arg = process.argv[2] || './API.md'; - return arg.split(','); +const args = process.argv.slice(2); +const files = (args.length ? args : ['README.md']).flatMap((arg) => arg.split(',')).filter(Boolean); + +let exitCode = 0; +for (const arg of files) { + try { + await processFile(resolve(repoRoot, arg)); + } catch (err) { + console.error(`${arg}: ${err.message}`); + exitCode = 1; + } } +process.exit(exitCode); + +/** @param {string} mdPath */ +async function processFile(mdPath) { + const source = await readFile(mdPath, 'utf8'); + + const inFence = (() => { + const fences = []; + let open = false; + for (const [i, line] of source.split('\n').entries()) { + if (/^```/.test(line)) { + open = !open; + } + fences[i] = open; + } + return (i) => fences[i]; + })(); + + const lines = source.split('\n'); + /** @type {Array<{ level: number, text: string, slug: string }>} */ + const headlines = []; + const slugCounts = new Map(); + let firstHeadlineIdx = -1; + let titleSkipped = false; + + for (const [i, line] of lines.entries()) { + if (inFence(i)) continue; + const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line); + if (!match) continue; + const level = match[1].length; + if (level === 1 && !titleSkipped) { + titleSkipped = true; + continue; + } + const text = match[2].replace(/\*/g, '\\*'); + const baseSlug = slugify(match[2]); + const n = slugCounts.get(baseSlug) ?? 0; + slugCounts.set(baseSlug, n + 1); + const slug = n === 0 ? baseSlug : `${baseSlug}-${n}`; + headlines.push({ level, text, slug }); + if (firstHeadlineIdx === -1) firstHeadlineIdx = i; + } -function generate(filename) { - const api = fs.readFileSync(filename, 'utf8'); - const tocOptions = { - bullets: '-', - slugify(text) { - return text - .toLowerCase() - .replace(/\s/g, '-') - .replace(/[^\w-]/g, ''); - }, - }; - - const output = Toc.insert(api, tocOptions).replace( - /(.|\n)*/, - '\n\n# ' + version + ' API Reference\n\n' - ); - - fs.writeFileSync(filename, output); + if (firstHeadlineIdx === -1 || headlines.length === 0) { + console.error(`${mdPath}: no non-title headlines found; nothing to do.`); + return; + } + + const minLevel = Math.min(...headlines.map((h) => h.level)); + const tocLines = headlines.map(({ level, text, slug }) => `${' '.repeat(level - minLevel)}- [${text}](#${slug})`); + const tocBlock = [TOC_START, '', ...tocLines, '', TOC_END].join('\n'); + + const startIdx = lines.findIndex((l) => l.trim() === TOC_START); + const endIdx = lines.findIndex((l) => l.trim() === TOC_END); + + let updated; + if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { + updated = [...lines.slice(0, startIdx), tocBlock, ...lines.slice(endIdx + 1)].join('\n'); + } else { + updated = [...lines.slice(0, firstHeadlineIdx), tocBlock, '', ...lines.slice(firstHeadlineIdx)].join('\n'); + } + + if (updated === source) { + console.log(`${mdPath}: TOC already up to date.`); + } else { + await writeFile(mdPath, updated); + console.log(`${mdPath}: wrote TOC with ${headlines.length} entries.`); + } } -filenames.forEach(generate); +/** + * @param {string} text + * @returns {string} + */ +function slugify(text) { + return text + .toLowerCase() + .replace(/[`*_~]/g, '') + .replace(/[^\w\s-]/g, '') + .trim() + .replace(/\s+/g, '-'); +} diff --git a/src/index.js b/src/index.js index 82b69dc..3c53c4c 100644 --- a/src/index.js +++ b/src/index.js @@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url'; import BpmnModdle from 'bpmn-moddle'; import * as Elements from 'bpmn-elements'; import { Broker } from 'smqp'; -import serializer, { deserialize, TypeResolver } from 'moddle-context-serializer'; +import { Serializer, deserialize, TypeResolver } from 'moddle-context-serializer'; import DebugLogger from './Logger.js'; import JavaScripts from './JavaScripts.js'; @@ -29,6 +29,10 @@ const kTypeResolver = Symbol.for('type resolver'); export default Engine; export { JavaScripts }; +/** + * BPMN 2.0 execution engine. + * @param {import('types').BpmnEngineOptions} [options] + */ export function Engine(options) { if (!(this instanceof Engine)) return new Engine(options); @@ -114,6 +118,12 @@ Object.defineProperties(Engine.prototype, { }, }); +/** + * Execute the loaded definitions. + * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {(err: Error, execution?: Execution) => void} [callback] + * @returns {Promise} + */ Engine.prototype.execute = async function execute(...args) { const [executeOptions, callback] = getOptionsAndCallback(...args); try { @@ -127,12 +137,19 @@ Engine.prototype.execute = async function execute(...args) { return execution._execute(executeOptions, callback); }; +/** @returns {Promise} */ Engine.prototype.stop = function stop() { const execution = this.execution; if (!execution) return; return execution.stop(); }; +/** + * Recover engine from saved state. + * @param {import('types').BpmnEngineExecutionState | null} savedState + * @param {import('types').BpmnEngineOptions} [recoverOptions] + * @returns {Engine} + */ Engine.prototype.recover = function recover(savedState, recoverOptions) { if (this[kExecution]?.isRunning) { throw new Error('cannot recover a running engine'); @@ -178,6 +195,12 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { return this; }; +/** + * Resume execution from a previously recovered state. + * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {(err: Error, execution?: Execution) => void} [callback] + * @returns {Promise} + */ Engine.prototype.resume = async function resume(...args) { const [resumeOptions, callback] = getOptionsAndCallback(...args); @@ -199,6 +222,10 @@ Engine.prototype.resume = async function resume(...args) { return execution._resume(resumeOptions, callback); }; +/** + * Add a pre-serialized source context to the engine. + * @param {{ sourceContext: import('types').SerializableContext }} [options] + */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; const loadedDefinitions = this[kLoadedDefinitions]; @@ -206,16 +233,25 @@ Engine.prototype.addSource = function addSource(options) { this[kPendingSources].add(options.sourceContext); }; +/** + * @param {import('types').BpmnEngineExecuteOptions} [executeOptions] + * @returns {Promise} + */ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { const loadedDefinitions = this[kLoadedDefinitions]; if (loadedDefinitions?.length) return Promise.resolve(loadedDefinitions); return this._loadDefinitions(executeOptions); }; +/** + * @param {string} id + * @returns {Promise} + */ Engine.prototype.getDefinitionById = async function getDefinitionById(id) { return (await this.getDefinitions()).find((d) => d.id === id); }; +/** @returns {Promise} */ Engine.prototype.getState = async function getState() { const execution = this.execution; if (execution) return execution.getState(); @@ -224,6 +260,11 @@ Engine.prototype.getState = async function getState() { return new Execution(this, definitions, this.options).getState(); }; +/** + * @template R + * @param {import('types').BpmnEngineEvent} eventName + * @returns {Promise} + */ Engine.prototype.waitFor = function waitFor(eventName) { const self = this; return new Promise((resolve, reject) => { @@ -241,12 +282,14 @@ Engine.prototype.waitFor = function waitFor(eventName) { }); }; +/** @internal */ Engine.prototype._loadDefinitions = async function loadDefinitions(executeOptions) { const runSources = await Promise.all(this[kPendingSources]); const loadedDefinitions = (this[kLoadedDefinitions] = runSources.map((source) => this._loadDefinition(source, executeOptions))); return loadedDefinitions; }; +/** @internal */ Engine.prototype._loadDefinition = function loadDefinition(serializedContext, executeOptions) { const environment = this.environment; const context = new Elements.Context( @@ -269,20 +312,29 @@ Engine.prototype._loadDefinition = function loadDefinition(serializedContext, ex return new Elements.Definition(context); }; +/** @internal */ Engine.prototype._serializeSource = async function serializeSource(source) { const moddleContext = await this._getModdleContext(source); return this._serializeModdleContext(moddleContext); }; +/** @internal */ Engine.prototype._serializeModdleContext = function serializeModdleContext(moddleContext) { - return serializer(moddleContext, this[kTypeResolver], this.options.extendFn); + return Serializer(moddleContext, this[kTypeResolver], this.options.extendFn); }; +/** @internal */ Engine.prototype._getModdleContext = function getModdleContext(source) { const bpmnModdle = new BpmnModdle(this.options.moddleOptions); return bpmnModdle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); }; +/** + * @param {Engine} engine + * @param {any[]} definitions + * @param {import('types').BpmnEngineExecuteOptions} [options] + * @param {boolean} [isRecovered] + */ export function Execution(engine, definitions, options, isRecovered = false) { this.name = engine.name; this.options = options; @@ -332,6 +384,7 @@ Object.defineProperties(Execution.prototype, { }, }); +/** @internal */ Execution.prototype._execute = function execute(executeOptions, callback) { this._setup(executeOptions); this[kStopped] = false; @@ -353,6 +406,7 @@ Execution.prototype._execute = function execute(executeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._resume = function resume(resumeOptions, callback) { this._setup(resumeOptions); @@ -366,6 +420,7 @@ Execution.prototype._resume = function resume(resumeOptions, callback) { return this; }; +/** @internal */ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callback) { if (!callback) return; @@ -416,6 +471,7 @@ Execution.prototype._addConsumerCallbacks = function addConsumerCallbacks(callba } }; +/** @returns {Promise} */ Execution.prototype.stop = async function stop() { const engine = this[kEngine]; const prom = engine.waitFor('stop'); @@ -433,6 +489,7 @@ Execution.prototype.stop = async function stop() { return result; }; +/** @internal */ Execution.prototype._setup = function setup(setupOptions) { const listener = setupOptions?.listener || this.options.listener; if (listener && typeof listener.emit !== 'function') throw new Error('listener.emit is not a function'); @@ -452,6 +509,7 @@ Execution.prototype._setup = function setup(setupOptions) { } }; +/** @internal */ Execution.prototype._onChildMessage = function onChildMessage(routingKey, message, owner) { const { environment: ownerEnvironment } = owner; const listener = ownerEnvironment.options?.listener; @@ -521,6 +579,7 @@ Execution.prototype._onChildMessage = function onChildMessage(routingKey, messag } }; +/** @internal */ Execution.prototype._complete = function complete(eventType, content, messageProperties) { const timers = this.environment.timers; timers.executing.slice().forEach((ref) => timers.clearTimeout(ref)); @@ -528,11 +587,13 @@ Execution.prototype._complete = function complete(eventType, content, messagePro return eventType !== 'error' && this[kEngine].emit(eventType, this); }; +/** @internal */ Execution.prototype._teardownDefinition = function teardownDefinition(definition) { this[kExecuting].delete(definition); definition.broker.cancel('_engine_definition'); }; +/** @internal */ Execution.prototype._saveOutput = function saveOutput(output) { if (!output || typeof output !== 'object') return; @@ -548,6 +609,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; +/** @returns {import('types').BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -567,6 +629,10 @@ Execution.prototype.getState = function getState() { }; }; +/** + * @param {string} activityId + * @returns {any} + */ Execution.prototype.getActivityById = function getActivityById(activityId) { for (const definition of this.definitions) { const activity = definition.getActivityById(activityId); @@ -574,6 +640,7 @@ Execution.prototype.getActivityById = function getActivityById(activityId) { } }; +/** @returns {any[]} */ Execution.prototype.getPostponed = function getPostponed() { const definitions = this.stopped ? this.definitions : this[kExecuting]; let result = []; @@ -583,6 +650,10 @@ Execution.prototype.getPostponed = function getPostponed() { return result; }; +/** + * @param {import('types').BpmnMessage} [payload] + * @param {{ ignoreSameDefinition?: boolean }} [signalOptions] + */ Execution.prototype.signal = function signal(payload, signalOptions) { for (const definition of this[kExecuting]) { if (signalOptions?.ignoreSameDefinition && payload?.parent?.id === definition.id) continue; @@ -590,26 +661,35 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; +/** @param {import('types').BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); } }; +/** + * @template T + * @param {import('types').BpmnEngineEvent} eventName + * @returns {Promise} + */ Execution.prototype.waitFor = function waitFor(...args) { return this[kEngine].waitFor(...args); }; +/** @internal */ Execution.prototype._onBrokerReturn = function onBrokerReturn(message) { if (message.properties.type === 'error') { this[kEngine].emit('error', message.content); } }; +/** @internal */ Execution.prototype._debug = function debug(msg) { this[kEngine].logger.debug(`<${this.name}> ${msg}`); }; +/** @internal */ Execution.prototype._getActivityStatus = function getActivityStatus() { let status = 'idle'; const executing = this[kExecuting]; diff --git a/test/feature/extend-feature.js b/test/feature/extend-feature.js index 0ef1c86..96b25e0 100644 --- a/test/feature/extend-feature.js +++ b/test/feature/extend-feature.js @@ -509,7 +509,7 @@ Feature('extending behaviour', () => { And('second flow was discarded', () => { const flow = execution.definitions[0].getProcesses()[0].context.getSequenceFlowById('flow3'); - expect(flow.counters).to.have.property('discard', 1); + expect(flow.counters).to.have.property('take', 0); }); Given('an engine with type resolver function with new behaviour', () => { @@ -547,9 +547,9 @@ Feature('extending behaviour', () => { expect(flow.counters).to.have.property('take', 1); }); - And('second flow was discarded', () => { + And('second flow was not taken', () => { const flow = execution.definitions[0].getProcesses()[0].context.getSequenceFlowById('flow3'); - expect(flow.counters).to.have.property('discard', 1); + expect(flow.counters).to.have.property('take', 0); }); }); diff --git a/test/feature/issues-feature.js b/test/feature/issues-feature.js index 2c27a14..e34cc7e 100644 --- a/test/feature/issues-feature.js +++ b/test/feature/issues-feature.js @@ -176,16 +176,14 @@ Feature('Issues', () => { return end; }); - And('user task was discarded once', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 1); }); - And('end was discarded thrice', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 3); }); When('executed again', async () => { @@ -229,16 +227,14 @@ Feature('Issues', () => { return end; }); - And('user task was discarded once', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 1); }); - And('end was discarded thrice', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 3); }); When('execution is recovered with state from first run user task wait', () => { @@ -279,16 +275,14 @@ Feature('Issues', () => { return end; }); - And('user task was discarded once', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 1); }); - And('end was discarded thrice', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 3); }); Given('ran again', async () => { @@ -336,8 +330,8 @@ Feature('Issues', () => { execution = await engine.resume(); }); - Then('end event is discarded once', () => { - expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 1 }); + Then('end event is not taken', () => { + expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); When('user task is signaled', () => { @@ -348,8 +342,8 @@ Feature('Issues', () => { return end; }); - Then('end event is taken once and discarded thrice', () => { - expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 1, discarded: 3 }); + Then('end event is taken once', () => { + expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 1, discarded: 0 }); }); }); @@ -419,16 +413,16 @@ Feature('Issues', () => { return end; }); - And('user task was discarded twice', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 2); + expect(task.counters).to.have.property('discarded', 0); }); - And('end was discarded four times', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 4); + expect(task.counters).to.have.property('discarded', 0); }); When('executed again', async () => { @@ -472,16 +466,16 @@ Feature('Issues', () => { return end; }); - And('user task was discarded twice', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 2); + expect(task.counters).to.have.property('discarded', 0); }); - And('end was discarded four times', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 4); + expect(task.counters).to.have.property('discarded', 0); }); When('execution is recovered with state from first run user task wait', () => { @@ -522,16 +516,16 @@ Feature('Issues', () => { return end; }); - And('user task was discarded twice', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 2); + expect(task.counters).to.have.property('discarded', 0); }); - And('end was discarded four times', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 4); + expect(task.counters).to.have.property('discarded', 0); }); Given('ran again', async () => { @@ -570,7 +564,7 @@ Feature('Issues', () => { end = engine.waitFor('end'); }); - Then('end event is still not discarded', async () => { + Then('end event is still not touched', async () => { const [definition] = await engine.getDefinitions(); expect(definition.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); @@ -579,8 +573,8 @@ Feature('Issues', () => { execution = await engine.resume(); }); - Then('end event is discarded once', () => { - expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 1 }); + Then('end event is not taken', () => { + expect(execution.getActivityById('End').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); When('user task is signaled', () => { @@ -591,16 +585,16 @@ Feature('Issues', () => { return end; }); - And('user task was taken once and discarded twice', () => { + And('user task was taken once', () => { const task = execution.getActivityById('UserTask'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 2); + expect(task.counters).to.have.property('discarded', 0); }); - And('end was taken once and discarded four times', () => { + And('end was taken once', () => { const task = execution.getActivityById('End'); expect(task.counters).to.have.property('taken', 1); - expect(task.counters).to.have.property('discarded', 4); + expect(task.counters).to.have.property('discarded', 0); }); }); }); @@ -718,8 +712,8 @@ Feature('Issues', () => { expect(execution.getActivityById('task3').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); - And('fourth user task was discarded', () => { - expect(execution.getActivityById('task4').counters).to.deep.equal({ taken: 0, discarded: 1 }); + And('fourth user task was not taken', () => { + expect(execution.getActivityById('task4').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); let end; @@ -753,8 +747,8 @@ Feature('Issues', () => { expect(execution.getActivityById('task3').counters).to.deep.equal({ taken: 1, discarded: 0 }); }); - And('fourth user task was discarded', () => { - expect(execution.getActivityById('task4').counters).to.deep.equal({ taken: 0, discarded: 1 }); + And('fourth user task was not taken', () => { + expect(execution.getActivityById('task4').counters).to.deep.equal({ taken: 0, discarded: 0 }); }); And('execution completed', () => { diff --git a/test/helpers/testHelpers.js b/test/helpers/testHelpers.js index fabc45c..03e2596 100644 --- a/test/helpers/testHelpers.js +++ b/test/helpers/testHelpers.js @@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url'; import BpmnModdle from 'bpmn-moddle'; import * as Elements from 'bpmn-elements'; import Logger from '../../src/Logger.js'; -import serializer, { TypeResolver } from 'moddle-context-serializer'; +import { Serializer, TypeResolver } from 'moddle-context-serializer'; const nodeRequire = createRequire(fileURLToPath(import.meta.url)); export const camundaBpmnModdle = nodeRequire('camunda-bpmn-moddle/resources/camunda.json'); @@ -25,7 +25,7 @@ export async function context(source, options) { ...options?.elements, }); - return serializer(moddleCtx, types, options?.extendFn); + return Serializer(moddleCtx, types, options?.extendFn); } export function moddleContext(source, options) { diff --git a/test/issues-test.js b/test/issues-test.js index b5ae0f9..0c92e7e 100644 --- a/test/issues-test.js +++ b/test/issues-test.js @@ -54,36 +54,43 @@ describe('issues', () => { expect(children.map(({ id }) => id)).to.eql(['Start', 'Parallel1', 'Task_A', 'Task_B', 'Parallel2', 'Task_C', 'End']); } + // eslint-disable-next-line no-useless-assignment let [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[0].definitions[0].execution.processes[0].execution.children; expect(Start, 'state 0 Start').to.have.property('status', 'started'); expect(Parallel1, 'state 0 Parallel1').to.not.have.property('status'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[1].definitions[0].execution.processes[0].execution.children; expect(Start, 'state 1 Start').to.have.property('status', 'end'); expect(Parallel1, 'state 1 Parallel1').to.have.property('status', 'started'); expect(Task_A, 'state 1 Task_A').to.not.have.property('status'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[2].definitions[0].execution.processes[0].execution.children; expect(Parallel1, 'state 2 Parallel1').to.have.property('status', 'end'); expect(Task_A, 'state 2 Task_A').to.have.property('status', 'started'); expect(Task_B, 'state 2 Task_B').to.not.have.property('status'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[3].definitions[0].execution.processes[0].execution.children; expect(Parallel1, 'state 3 Parallel1').to.have.property('status', 'end'); - expect(Task_A, 'state 3 Task_A').to.not.have.property('status'); - expect(Task_B, 'state 3 Task_B').to.have.property('status', 'started'); - expect(Parallel2, 'state 3 Parallel2').to.not.have.property('status'); + expect(Task_A, 'state 3 Task_A').to.have.property('status', 'end'); + expect(Task_B, 'state 3 Task_B').to.not.have.property('status'); + expect(Parallel2, 'state 3 Parallel2').to.have.property('status', 'started'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[4].definitions[0].execution.processes[0].execution.children; expect(Task_A, 'state 4 Task_A').to.not.have.property('status'); - expect(Task_B, 'state 4 Task_B').to.have.property('status', 'end'); - expect(Parallel2, 'state 4 Parallel2').to.have.property('status', 'started'); + expect(Task_B, 'state 4 Task_B').to.have.property('status', 'started'); + expect(Parallel2, 'state 4 Parallel2').to.have.property('status', 'executing'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[5].definitions[0].execution.processes[0].execution.children; expect(Parallel2, 'state 5 Parallel2').to.have.property('status', 'end'); expect(Task_C, 'state 5 Task_C').to.have.property('status', 'started'); expect(End, 'state 5 End').to.not.have.property('status'); + // eslint-disable-next-line no-useless-assignment [Start, Parallel1, Task_A, Task_B, Parallel2, Task_C, End] = states[6].definitions[0].execution.processes[0].execution.children; expect(Task_C, 'state 6 Task_C').to.have.property('status', 'end'); expect(End, 'state 6 End').to.have.property('status', 'started'); diff --git a/tsconfig.json b/tsconfig.json index 8201c9f..115d6b8 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,29 +1,25 @@ { - "include": ["src/**/*", "types"], + "include": ["src/**/*", "types/interfaces.d.ts"], "compilerOptions": { - "emitDeclarationOnly": true, - "sourceMap": false, + "noEmit": true, "rootDir": "src", - "lib": ["es2017"], - "target": "es2017", - "outDir": "./tmp/ignore", - "declaration": true, - "noEmitOnError": true, + "lib": ["es2022"], + "target": "es2022", "noErrorTruncation": true, + "skipLibCheck": true, "allowJs": true, "checkJs": false, - "module": "esnext", - "moduleResolution": "node", - "resolveJsonModule": false, - "allowSyntheticDefaultImports": true, + "module": "nodenext", + "moduleResolution": "nodenext", + "resolveJsonModule": true, "strict": true, + "strictNullChecks": false, "stripInternal": true, "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, - "typeRoots": ["./node_modules/@types"], "paths": { - "types": ["./types/bpmn-engine.js"] + "types": ["./types/interfaces.d.ts"] } } } diff --git a/types/bpmn-engine.d.ts b/types/bpmn-engine.d.ts deleted file mode 100644 index d61a035..0000000 --- a/types/bpmn-engine.d.ts +++ /dev/null @@ -1,334 +0,0 @@ -// Author : Saeed Tabrizi -// Refactored by : Pål Edman - -import { EventEmitter } from 'node:events'; -import { Definitions as BpmnModdleDefinitions } from 'bpmn-moddle'; -import { extendFn, SerializableContext, TypeResolver } from 'moddle-context-serializer'; -import { - ActivityStatus, - ElementBroker, - EnvironmentOptions, - Definition, - DefinitionState, - Environment, - Api, - ElementBase, - ILogger, - EnvironmentState, - IScripts, - Script, -} from 'bpmn-elements'; - -declare module 'bpmn-engine' { - /** - * Engine emits the following events: - - error: An non-recoverable error has occurred - stop: Executions was stopped - end: Execution completed - */ - export type BpmnEngineEvent = 'error' | 'stop' | 'end'; - /** - * Each activity and flow emits events when changing state. - - activity.enter: An activity is entered - activity.start: An activity is started - activity.wait: The activity is postponed for some reason, e.g. a user task is waiting to be signaled or a message is expected - wait: Same as above - activity.end: An activity has ended successfully - activity.leave: The execution left the activity - activity.stop: Activity run was stopped - activity.throw: An recoverable error was thrown - activity.error: An non-recoverable error has occurred - */ - export type BpmnActivityEvent = - | 'activity.enter' - | 'activity.start' - | 'activity.wait' - | 'wait' - | 'activity.end' - | 'activity.leave' - | 'activity.stop' - | 'activity.throw' - | 'activity.error'; - /** - * Sequence flow events - flow.take: The sequence flow was taken - flow.discard: The sequence flow was discarded - flow.looped: The sequence is looped - */ - export type BpmnSequenceFlowEvent = 'flow.take' | 'flow.discard' | 'flow.looped'; - - export interface BpmnMessage { - id?: string; - executionId?: string; - [name: string]: any; - } - - export interface IListenerEmitter { - emit(eventName: string, ...args: any[]): void; - } - - export interface BpmnEngineExecuteOptions extends EnvironmentOptions { - listener?: EventEmitter | IListenerEmitter; - } - - export interface BpmnEngineOptions extends BpmnEngineExecuteOptions { - /** - * optional name of engine, - */ - name?: string; - /** - * optional BPMN 2.0 definition source as string - */ - source?: string; - /** - * optional serialized context supplied by moddle-context-serializer - */ - sourceContext?: SerializableContext; - /** - * optional override of BPMN Elements behaviors, defaults to bpmn-elements - */ - elements?: Record; - /** - * Passed to moddle-context-serializer as TypeResolver - */ - typeResolver?: typeof TypeResolver; - /** - * Passed to moddle-context-serializer as extendFn - */ - extendFn?: extendFn; - /** - * optional bpmn-moddle options to be passed to bpmn-moddle - */ - moddleOptions?: any; - moddleContext?: BpmnModdleDefinitions; - [x: string]: any; - } - - const enum BpmnEngineRunningStatus { - /** - * Engine is not executing - */ - Idle = 'idle', - /** - * Engine is executing - */ - Running = 'running', - /** - * Engine execution is stopped - */ - Stopped = 'stopped', - /** - * Engine execution has errored - */ - Error = 'error', - } - - export function Engine(options?: BpmnEngineOptions): Engine; - export class Engine extends EventEmitter { - constructor(options?: BpmnEngineOptions); - /** - * engine name - */ - readonly name: string; - /** - * engine broker - */ - broker: ElementBroker; - /** - * engine state - */ - readonly state: BpmnEngineRunningStatus; - /** - * boolean stopped - */ - readonly stopped: boolean; - /** - * current engine execution - */ - readonly execution: Execution; - /** - * engine environment - */ - readonly environment: Environment; - /** - * engine logger - */ - readonly logger: ILogger; - get activityStatus(): ActivityStatus; - - /** - * execute definition - * @param options Optional object with options to override the initial engine options - * @param cb Callback called before the throw - * @summary Execute options overrides the initial options passed to the engine before executing the definition. - */ - execute(): Promise; - execute(options: BpmnEngineExecuteOptions): Promise; - execute(options: BpmnEngineExecuteOptions, cb: (err: Error, execution?: Execution) => void): Promise; - execute(cb: (err: Error) => void): Promise; - - /** - * get definition by id - * @param id id of definition - */ - getDefinitionById(id: string): Promise; - - /** - * get all definitions - */ - getDefinitions(): Promise; - - /** - * get execution serialized state - */ - getState(): Promise; - - /** - * Recover engine from state - * @param state engine state - * @param recoverOptions optional object with options that will completely override the options passed to the engine at init - */ - recover(state: any, recoverOptions?: BpmnEngineOptions): Engine; - - /** - * Resume execution function with previously saved engine state. - * @param options - * @param callback - */ - resume(options?: BpmnEngineExecuteOptions, callback?: (err: Error, execution?: Execution) => void): Promise; - - /** - * Stop execution. The instance is terminated. - */ - stop(): Promise; - - waitFor(eventName: BpmnEngineEvent): Promise; - - /** - * Add definition source by source context. - * @param options - */ - addSource(options?: { sourceContext: any }): void; - } - - interface BpmnEngineDefinitionState extends DefinitionState { - /** - * Serialized moddle-context-serializer context - */ - source?: string; - } - - export interface BpmnEngineExecutionState { - name: string; - engineVersion: string; - state: BpmnEngineRunningStatus; - stopped: boolean; - environment: EnvironmentState; - definitions: BpmnEngineDefinitionState[]; - } - - export interface Execution { - /** - * engine name - * - * @type {string} - * @ - */ - readonly name: string; - /** - * state of execution, i.e running or idle - * - * @type {("running"| "idle")} - * - */ - readonly state: 'running' | 'idle'; - /** - * is the execution stopped - * - * @type {boolean} - * - */ - readonly stopped: boolean; - /** - * engine environment - * - * @type {Environment} - * - */ - readonly environment: Environment; - - /** - * executing definitions - * - * @type {Definition} - * - */ - readonly definitions: Definition[]; - get activityStatus(): ActivityStatus; - /** - * Are any definition running - */ - get isRunning(): boolean; - - /** - * Get activity/element by id. Loops the definitions and returns the first found activity with id. - * @param activityId Activity or element id - */ - getActivityById(activityId: string): ElementBase; - getActivityById(activityId: string): R; - - /** - * get execution serializable state - * - * @returns {BpmnEngineExecutionState} - * - */ - getState(): BpmnEngineExecutionState; - - /** - * stop execution - * - * - */ - stop(): Promise; - - /** - * get activities in a postponed state - * - * - */ - getPostponed(): Api[]; - - /** - * send signal to execution, distributed to all definitions - * Delegate a signal message to all interested parties, usually MessageEventDefinition, SignalEventDefinition, SignalTask (user, manual), ReceiveTask, or a StartEvent that has a form. - * @param message {BpmnMessage} - * @param options - */ - signal(message?: BpmnMessage, options?: { ignoreSameDefinition?: boolean }): void; - - /** - * send cancel activity to execution, distributed to all definitions - * Delegate a cancel message to all interested parties, perhaps a stalled TimerEventDefinition. - * @param message - */ - cancelActivity(message?: BpmnMessage): void; - - /** - * - * @param event - */ - waitFor(event: BpmnEngineEvent): Promise; - } - - export class JavaScripts implements IScripts { - /** - * @param disableDummy Disable returning dummy scripts if script is not found. Dummy script will immediately call next function without error or return value - */ - constructor(disableDummy?: boolean); - register(activity: any): Script | undefined; - getScript(language: string, identifier: { id: string; [x: string]: any }): Script; - } -} diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..935a6cd --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,153 @@ +declare module 'bpmn-engine' { + import type { EventEmitter } from 'node:events'; + import type { Broker } from 'smqp'; + import type { Definitions as BpmnModdleDefinitions } from 'bpmn-moddle'; + import type { ExtendFn, SerializableContext, ResolverFn } from 'moddle-context-serializer'; + import type { + ActivityStatus, + Definition, + ElementBase, + Environment, + EnvironmentOptions, + EnvironmentState, + IApi, + ILogger, + IScripts, + Script, + } from 'bpmn-elements'; + + export type BpmnEngineEvent = 'error' | 'stop' | 'end'; + + export type BpmnActivityEvent = + | 'activity.enter' + | 'activity.start' + | 'activity.wait' + | 'wait' + | 'activity.end' + | 'activity.leave' + | 'activity.stop' + | 'activity.throw' + | 'activity.error'; + + export type BpmnSequenceFlowEvent = 'flow.take' | 'flow.discard' | 'flow.looped'; + + export type BpmnEngineRunningStatus = 'idle' | 'running' | 'stopped' | 'error'; + + export interface BpmnMessage { + id?: string; + executionId?: string; + [name: string]: any; + } + + export interface IListenerEmitter { + emit(eventName: string, ...args: any[]): void; + } + + export interface BpmnEngineExecuteOptions extends EnvironmentOptions { + listener?: EventEmitter | IListenerEmitter; + } + + export interface BpmnEngineOptions extends BpmnEngineExecuteOptions { + name?: string; + source?: string; + sourceContext?: SerializableContext; + elements?: Record; + typeResolver?: ResolverFn; + extendFn?: ExtendFn; + moddleOptions?: any; + moddleContext?: BpmnModdleDefinitions; + Logger?: (scope: string) => ILogger; + scripts?: IScripts; + disableDummyScript?: boolean; + [x: string]: any; + } + + export interface BpmnEngineDefinitionState { + state: string; + source?: string; + [x: string]: any; + } + + export interface BpmnEngineExecutionState { + name: string; + engineVersion: string; + state: BpmnEngineRunningStatus; + stopped: boolean; + environment: EnvironmentState; + definitions: BpmnEngineDefinitionState[]; + } + + export function Engine(options?: BpmnEngineOptions): Engine; + export class Engine extends EventEmitter { + constructor(options?: BpmnEngineOptions); + options: BpmnEngineOptions; + readonly name: string; + readonly broker: Broker; + readonly logger: ILogger; + readonly environment: Environment; + readonly state: BpmnEngineRunningStatus; + readonly stopped: boolean; + readonly execution: Execution | null; + readonly activityStatus: ActivityStatus; + + execute(): Promise; + execute(options: BpmnEngineExecuteOptions): Promise; + execute(options: BpmnEngineExecuteOptions, callback: (err: Error | null, execution?: Execution) => void): Promise; + execute(callback: (err: Error | null, execution?: Execution) => void): Promise; + + getDefinitionById(id: string): Promise; + getDefinitions(executeOptions?: BpmnEngineExecuteOptions): Promise; + getState(): Promise; + + recover(savedState: BpmnEngineExecutionState | null, recoverOptions?: BpmnEngineOptions): Engine; + + resume(): Promise; + resume(options: BpmnEngineExecuteOptions): Promise; + resume(options: BpmnEngineExecuteOptions, callback: (err: Error | null, execution?: Execution) => void): Promise; + resume(callback: (err: Error | null, execution?: Execution) => void): Promise; + + stop(): Promise | undefined; + + addSource(options?: { sourceContext: SerializableContext }): void; + + waitFor(eventName: BpmnEngineEvent): Promise; + } + + export function Execution( + engine: Engine, + definitions: Definition[], + options?: BpmnEngineExecuteOptions, + isRecovered?: boolean + ): Execution; + export class Execution { + constructor(engine: Engine, definitions: Definition[], options?: BpmnEngineExecuteOptions, isRecovered?: boolean); + options: BpmnEngineExecuteOptions; + readonly name: string; + readonly definitions: Definition[]; + readonly broker: Broker; + readonly environment: Environment; + readonly state: BpmnEngineRunningStatus; + readonly stopped: boolean; + readonly isRunning: boolean; + readonly activityStatus: ActivityStatus; + + getActivityById(activityId: string): ElementBase | undefined; + getActivityById(activityId: string): R | undefined; + getPostponed(): IApi[]; + getState(): BpmnEngineExecutionState; + stop(): Promise; + + signal(message?: BpmnMessage, options?: { ignoreSameDefinition?: boolean }): void; + cancelActivity(message?: BpmnMessage): void; + + waitFor(eventName: BpmnEngineEvent): Promise; + } + + export class JavaScripts implements IScripts { + constructor(disableDummy?: boolean); + register(activity: any): Script | undefined; + getScript(language: string, identifier: { id: string; [x: string]: any }): Script; + } + + export default Engine; +} diff --git a/types/interfaces.d.ts b/types/interfaces.d.ts new file mode 100644 index 0000000..ee93701 --- /dev/null +++ b/types/interfaces.d.ts @@ -0,0 +1,80 @@ +import type { EventEmitter } from 'node:events'; +import type { Definitions as BpmnModdleDefinitions } from 'bpmn-moddle'; +import type { ExtendFn, SerializableContext, ResolverFn } from 'moddle-context-serializer'; +import type { ActivityStatus, ElementBroker, EnvironmentOptions, EnvironmentState, IScripts, Script, ILogger } from 'bpmn-elements'; + +export type { SerializableContext }; + +export type BpmnEngineEvent = 'error' | 'stop' | 'end'; + +export type BpmnActivityEvent = + | 'activity.enter' + | 'activity.start' + | 'activity.wait' + | 'wait' + | 'activity.end' + | 'activity.leave' + | 'activity.stop' + | 'activity.throw' + | 'activity.error'; + +export type BpmnSequenceFlowEvent = 'flow.take' | 'flow.discard' | 'flow.looped'; + +export type BpmnEngineRunningStatus = 'idle' | 'running' | 'stopped' | 'error'; + +export interface BpmnMessage { + id?: string; + executionId?: string; + [name: string]: any; +} + +export interface IListenerEmitter { + emit(eventName: string, ...args: any[]): void; +} + +export interface BpmnEngineExecuteOptions extends EnvironmentOptions { + listener?: EventEmitter | IListenerEmitter; +} + +export interface BpmnEngineOptions extends BpmnEngineExecuteOptions { + name?: string; + source?: string; + sourceContext?: SerializableContext; + elements?: Record; + typeResolver?: ResolverFn; + extendFn?: ExtendFn; + moddleOptions?: any; + moddleContext?: BpmnModdleDefinitions; + Logger?: (scope: string) => ILogger; + scripts?: IScripts; + disableDummyScript?: boolean; + [x: string]: any; +} + +export interface BpmnEngineDefinitionState { + state: string; + source?: string; + [x: string]: any; +} + +export interface BpmnEngineExecutionState { + name: string; + engineVersion: string; + state: BpmnEngineRunningStatus; + stopped: boolean; + environment: EnvironmentState; + definitions: BpmnEngineDefinitionState[]; +} + +export type { + ActivityStatus, + BpmnModdleDefinitions, + ElementBroker, + EnvironmentOptions, + EnvironmentState, + ExtendFn, + ILogger, + IScripts, + ResolverFn, + Script, +}; From 1ac92cda68a60eaec8b6c0928c2ba77b983b7aac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 13 Jun 2026 08:47:42 +0200 Subject: [PATCH 03/14] yaaaargs --- lib/index.cjs | 41 +++++++++++++++-------------------------- package.json | 8 ++++++++ 2 files changed, 23 insertions(+), 26 deletions(-) diff --git a/lib/index.cjs b/lib/index.cjs index 73d756f..b61029e 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -161,17 +161,6 @@ ProcessOutputDataObject.prototype.write = function writeDataObject(broker, excha return broker.publish(exchange, `${routingKeyPrefix}response`, { id, name, type, value }, messageProperties); }; -/** - * @typedef {import('types').BpmnEngineOptions} BpmnEngineOptions - * @typedef {import('types').BpmnEngineExecuteOptions} BpmnEngineExecuteOptions - * @typedef {import('types').BpmnEngineExecutionState} BpmnEngineExecutionState - * @typedef {import('types').BpmnEngineEvent} BpmnEngineEvent - * @typedef {import('types').BpmnEngineRunningStatus} BpmnEngineRunningStatus - * @typedef {import('types').BpmnMessage} BpmnMessage - * @typedef {import('types').SerializableContext} SerializableContext - */ - - const nodeRequire = node_module.createRequire(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))); const { version: engineVersion } = nodeRequire('../package.json'); @@ -277,7 +266,7 @@ Object.defineProperties(Engine.prototype, { /** * Execute the loaded definitions. - * @param {BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] * @param {(err: Error, execution?: Execution) => void} [callback] * @returns {Promise} */ @@ -303,8 +292,8 @@ Engine.prototype.stop = function stop() { /** * Recover engine from saved state. - * @param {BpmnEngineExecutionState | null} savedState - * @param {BpmnEngineOptions} [recoverOptions] + * @param {import('types').BpmnEngineExecutionState | null} savedState + * @param {import('types').BpmnEngineOptions} [recoverOptions] * @returns {Engine} */ Engine.prototype.recover = function recover(savedState, recoverOptions) { @@ -354,7 +343,7 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { /** * Resume execution from a previously recovered state. - * @param {BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] * @param {(err: Error, execution?: Execution) => void} [callback] * @returns {Promise} */ @@ -381,7 +370,7 @@ Engine.prototype.resume = async function resume(...args) { /** * Add a pre-serialized source context to the engine. - * @param {{ sourceContext: SerializableContext }} [options] + * @param {{ sourceContext: import('types').SerializableContext }} [options] */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; @@ -391,8 +380,8 @@ Engine.prototype.addSource = function addSource(options) { }; /** - * @param {BpmnEngineExecuteOptions} [executeOptions] - * @returns {Promise} + * @param {import('types').BpmnEngineExecuteOptions} [executeOptions] + * @returns {Promise} */ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { const loadedDefinitions = this[kLoadedDefinitions]; @@ -402,13 +391,13 @@ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { /** * @param {string} id - * @returns {Promise} + * @returns {Promise} */ Engine.prototype.getDefinitionById = async function getDefinitionById(id) { return (await this.getDefinitions()).find((d) => d.id === id); }; -/** @returns {Promise} */ +/** @returns {Promise} */ Engine.prototype.getState = async function getState() { const execution = this.execution; if (execution) return execution.getState(); @@ -419,7 +408,7 @@ Engine.prototype.getState = async function getState() { /** * @template R - * @param {BpmnEngineEvent} eventName + * @param {import('types').BpmnEngineEvent} eventName * @returns {Promise} */ Engine.prototype.waitFor = function waitFor(eventName) { @@ -489,7 +478,7 @@ Engine.prototype._getModdleContext = function getModdleContext(source) { /** * @param {Engine} engine * @param {any[]} definitions - * @param {BpmnEngineExecuteOptions} [options] + * @param {import('types').BpmnEngineExecuteOptions} [options] * @param {boolean} [isRecovered] */ function Execution(engine, definitions, options, isRecovered = false) { @@ -766,7 +755,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; -/** @returns {BpmnEngineExecutionState} */ +/** @returns {import('types').BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -808,7 +797,7 @@ Execution.prototype.getPostponed = function getPostponed() { }; /** - * @param {BpmnMessage} [payload] + * @param {import('types').BpmnMessage} [payload] * @param {{ ignoreSameDefinition?: boolean }} [signalOptions] */ Execution.prototype.signal = function signal(payload, signalOptions) { @@ -818,7 +807,7 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; -/** @param {BpmnMessage} [payload] */ +/** @param {import('types').BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); @@ -827,7 +816,7 @@ Execution.prototype.cancelActivity = function cancelActivity(payload) { /** * @template T - * @param {BpmnEngineEvent} eventName + * @param {import('types').BpmnEngineEvent} eventName * @returns {Promise} */ Execution.prototype.waitFor = function waitFor(...args) { diff --git a/package.json b/package.json index fc601ad..bcfd625 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,14 @@ "url": "https://github.com/paed01/bpmn-engine/master/LICENSE" } ], + "overrides": { + "c8": { + "yargs": "^18.0.0" + }, + "mocha": { + "yargs": "^18.0.0" + } + }, "devDependencies": { "@bonniernews/hot-bev": "^0.4.0", "@eslint/js": "^10.0.1", From a849aa5f960c9b2b5da0952845751be028314231 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 13 Jun 2026 08:58:18 +0200 Subject: [PATCH 04/14] bump appveyor builds --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 4871d7f..c4d9d6e 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,7 +1,7 @@ environment: matrix: - - nodejs_version: '18' - nodejs_version: '20' + - nodejs_version: '22' platform: - x64 From b4a9f955ee781290a9591907ae737b337c0f3839 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 13 Jun 2026 09:29:14 +0200 Subject: [PATCH 05/14] appveyor yaaargs! --- appveyor.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index c4d9d6e..47b2a20 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,6 +1,5 @@ environment: matrix: - - nodejs_version: '20' - nodejs_version: '22' platform: From 80ccd20619902195df456db9680823a887bc7aa3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Tue, 7 Jul 2026 10:17:22 +0200 Subject: [PATCH 06/14] move deps to peer, multiple versions are supported --- .mocharc.json | 1 + AGENTS.md | 2 +- CHANGELOG.md | 5 + README.md | 30 ++++++ docs/bpmn-moddle.md | 48 +++++++++ eslint.config.js | 5 +- package.json | 17 ++-- test/Engine-test.js | 1 + ...n-moddle-backward-compatibility-feature.js | 97 +++++++++++++++++++ test/feature/extend-feature.js | 2 +- test/feature/issues-feature.js | 4 +- 11 files changed, 199 insertions(+), 13 deletions(-) create mode 100644 docs/bpmn-moddle.md create mode 100644 test/feature/bpmn-moddle-backward-compatibility-feature.js diff --git a/.mocharc.json b/.mocharc.json index c6c2062..3a6a52d 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -3,5 +3,6 @@ "reporter": "spec", "timeout": 1000, "ui": "mocha-cakes-2", + "spec": ["test/**/*-feature.js", "test/**/*-test.js"], "require": ["chai/register-expect.js"] } diff --git a/AGENTS.md b/AGENTS.md index acb241e..e7f5a67 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -43,7 +43,7 @@ smqp (peer dep) ── message broker driving execution - **Symbol-keyed private state.** All instance internals on `Engine`/execution use `Symbol.for('engine')`, `Symbol.for('environment')`, etc. This is the project's privacy convention — don't replace with `#fields` (would break `Object.assign`-based extension and the recover/resume state walking). - **`getOptionsAndCallback.js`** — small helper that lets every public method accept either `(options, callback)` or `(callback)` style. Preserves the legacy callback-friendly API while the implementation returns Promises. -- **Extensions live under `src/extensions/`.** `ProcessOutputDataObject.js` is the canonical example — extensions plug into `bpmn-elements` via the engine's `extensions` option and are typically called for specific element types. +- **Extensions live under `src/extensions/`.** `ProcessOutputDataObject.js` is the canonical example — extensions plug into `bpmn-elements` via the engine's `extensions` option and are typically called for specific element types. The wider BPMN ecosystem — docs, guides, and additional extensions for this stack — is published at [0dep.se](https://0dep.se); check there before writing a new extension from scratch. - **Test fixtures under `test/resources/`** are real `.bpmn` XML files plus a few `.json` extensions. `test/resources/JsExtension.js` and `test/resources/js-bpmn-moddle.json` define a custom moddle extension namespace used across multiple feature tests. - **`mocha-cakes-2` ≠ standard mocha.** Files under `test/feature/` use Gherkin-flavored globals (`Feature`, `Scenario`, `Given`, `When`, `Then`, `And`, `But`). They're ESLint-allowed via the eslint config's globals. diff --git a/CHANGELOG.md b/CHANGELOG.md index 02a152f..42b839c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v26.0.1 - 2026-07-07 + +- declare `bpmn-elements`, `bpmn-moddle`, `moddle-context-serializer`, and `debug` as peer dependencies (install and pin them yourself) +- verify `bpmn-moddle` 9 and 10 both work — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) + ## v26.0.0 - 2026-06-13 ### Breaking diff --git a/README.md b/README.md index 323f4c7..67ce2e6 100644 --- a/README.md +++ b/README.md @@ -12,17 +12,47 @@ BPMN 2.0 execution engine. Open source javascript workflow engine. - [Changelog](/CHANGELOG.md) - [Examples](/docs/Examples.md) - [Upgrade version](/docs/Upgrade.md) +- [Peer dependencies](#peer-dependencies) +- [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) - [Supported elements](#supported-elements) +- [Extensions](#extensions) - [Debug](#debug) - [Example process](#a-pretty-image-of-a-process) - [Acknowledgments](#acknowledgments) +# Peer dependencies + +The runtime dependencies are declared as **peer dependencies**, so you install and pin them yourself and the engine reuses your single copy: + +```sh +npm install bpmn-engine bpmn-elements bpmn-moddle moddle-context-serializer debug smqp +``` + +| Peer dependency | Range | Role | +| ---------------------------------------------------------------------------------- | --------- | ------------------------------------------------- | +| [`bpmn-elements`](https://github.com/paed01/bpmn-elements) | `>=18` | element behaviour functions | +| [`bpmn-moddle`](https://github.com/bpmn-io/bpmn-moddle) | `>=9` | BPMN XML parser ([9 vs 10](/docs/bpmn-moddle.md)) | +| [`moddle-context-serializer`](https://github.com/paed01/moddle-context-serializer) | `>=6` | persistable source context | +| [`smqp`](https://github.com/paed01/smqp) | `^13.0.1` | message broker driving execution | +| [`debug`](https://github.com/debug-js/debug) | `>=4` | logging | + +`bpmn-moddle` spans a major version on purpose — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) for the import change and how persisted state stays compatible across the upgrade. + # Supported elements See [bpmn-elements](https://github.com/paed01/bpmn-elements) for supported elements. The engine only support elements and attributes included in the BPMN 2.0 scheme, but can be extended to understand other schemas and elements. The aim is to, at least, have BPMN 2.0 [core support](https://www.omg.org/bpmn/Samples/Elements/Core_BPMN_Elements.htm). +# Extensions + +The engine can be extended to understand other schemas and elements. Docs, guides, and ready-made extensions for the BPMN stack are published at [0dep.se](https://0dep.se) — check there before writing a new extension from scratch. + +Two ready-made extensions: + +- [`@0dep/bpmn-extensions`](https://github.com/zerodep/bpmn-extensions) +- [`@onify/flow-extensions`](https://github.com/onify/flow-extensions) + # Debug This package is shipped with [debug](https://github.com/debug-js/debug) activated with environment variable `DEBUG=bpmn-engine:*`. You can also provide your own logger. diff --git a/docs/bpmn-moddle.md b/docs/bpmn-moddle.md new file mode 100644 index 0000000..75a4354 --- /dev/null +++ b/docs/bpmn-moddle.md @@ -0,0 +1,48 @@ +# bpmn-moddle 9 vs 10 + +`bpmn-engine` declares [`bpmn-moddle`](https://github.com/bpmn-io/bpmn-moddle) as a **peer dependency** with the range `>=9`. You install it yourself and you are free to run the engine on either the 9.x or 10.x line: + +```sh +npm install bpmn-engine bpmn-moddle smqp +``` + +The engine uses `bpmn-moddle` only to parse BPMN XML into a moddle context (`new BpmnModdle().fromXML(source)`). The moddle context is then handed to [`moddle-context-serializer`](https://github.com/paed01/moddle-context-serializer) to produce the persistable source context. Everything downstream of the parse — execution, `getState()`, `recover()`, `resume()` — is independent of the `bpmn-moddle` version. + +## What changed in 10 + +The output of `fromXML()` is unchanged between 9 and 10 — same `{ rootElement, elementsById, references, warnings }` shape, same element ids and references. The breaking change is purely how the package is consumed: + +| | bpmn-moddle 9 | bpmn-moddle 10 | +| ------------- | -------------------------------------- | ------------------------------------------ | +| Module entry | CommonJS | ESM (`type: module`) | +| Parser export | **default** export | **named** export `BpmnModdle` | +| Import | `import BpmnModdle from 'bpmn-moddle'` | `import { BpmnModdle } from 'bpmn-moddle'` | +| `require()` | `require('bpmn-moddle')` | not available (ESM only) | + +```js +// bpmn-moddle 9 +import BpmnModdle from 'bpmn-moddle'; + +// bpmn-moddle 10 +import { BpmnModdle } from 'bpmn-moddle'; +``` + +`bpmn-engine`'s own source still imports the v9 default export (`import BpmnModdle from 'bpmn-moddle'`). If you let the engine parse XML for you (the `source` option, or `Engine.prototype._getModdleContext`), stay on the 9.x line. To run on 10.x, parse the XML yourself with the named export and pass the result to the engine via the `moddleContext` option: + +```js +import { Engine } from 'bpmn-engine'; +import { BpmnModdle } from 'bpmn-moddle'; // v10 + +const moddleContext = await new BpmnModdle().fromXML(source); +const engine = new Engine({ name: 'on v10', moddleContext }); +``` + +## Backward compatibility of persisted state + +Because the serialized source context and the runtime state are independent of the `bpmn-moddle` version, **state saved by a deployment running bpmn-moddle 9 keeps resuming after an upgrade to bpmn-moddle 10** (and vice versa). `recover()` deserializes the embedded source context — or a source context you re-parse with the new version — without ever calling `bpmn-moddle` itself. + +This is pinned down by the feature test [`test/feature/bpmn-moddle-backward-compatibility-feature.js`](../test/feature/bpmn-moddle-backward-compatibility-feature.js), which: + +1. starts the _mother-of-all_ process from a context parsed with bpmn-moddle **9**, stops it at the first user task wait and saves the state; +2. re-parses the same source with bpmn-moddle **10**, recovers the v9 runtime state against the v10 source context, and resumes it to completion; +3. asserts that the source context serialized via 9 and via 10 are byte-for-byte equal. diff --git a/eslint.config.js b/eslint.config.js index 57280c9..0dd4557 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -10,7 +10,8 @@ const rules = { 'no-array-constructor': 2, 'no-caller': 2, 'no-catch-shadow': 2, - 'no-console': 1, + 'no-console': 'error', + 'no-duplicate-imports': 'error', 'no-eval': 2, 'no-extend-native': 2, 'no-extra-bind': 2, @@ -43,7 +44,7 @@ const rules = { 'no-undef': 2, 'no-underscore-dangle': 0, 'no-unused-expressions': 2, - 'no-unused-vars': 2, + 'no-unused-vars': ['error', { ignoreRestSiblings: true }], 'no-use-before-define': [ 'error', { diff --git a/package.json b/package.json index bcfd625..56c0f25 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bpmn-engine", "description": "BPMN 2.0 execution engine. Open source javascript workflow engine.", - "version": "26.0.0", + "version": "26.0.1", "type": "module", "module": "./src/index.js", "main": "./lib/index.cjs", @@ -72,25 +72,28 @@ "@types/bpmn-moddle": "^10.0.0", "@types/node": "^20.19.40", "bent": "^7.3.12", + "bpmn-elements": "^18.0.0", + "bpmn-moddle": "^9.0.4", + "bpmn-moddle-10": "npm:bpmn-moddle@^10.0.0", "c8": "^11.0.0", "camunda-bpmn-moddle": "^7.0.1", "chai": "^6.2.0", "chronokinesis": "^8.0.0", + "debug": "^4.4.3", "eslint": "^10.5.0", "globals": "^17.6.0", + "moddle-context-serializer": "^6.0.0", "mocha": "^11.0.1", "mocha-cakes-2": "^3.3.0", "prettier": "^3.2.5", "rollup": "^4.10.0", "texample": "^1.0.2" }, - "dependencies": { - "bpmn-elements": "^18.0.0", - "bpmn-moddle": "^9.0.4", - "debug": "^4.4.3", - "moddle-context-serializer": "^6.0.0" - }, "peerDependencies": { + "bpmn-elements": ">=18", + "bpmn-moddle": ">=9", + "debug": ">=4", + "moddle-context-serializer": ">=6", "smqp": "^13.0.1" } } diff --git a/test/Engine-test.js b/test/Engine-test.js index bd2e421..92b3b9b 100644 --- a/test/Engine-test.js +++ b/test/Engine-test.js @@ -1,5 +1,6 @@ import { EventEmitter } from 'node:events'; import * as Bpmn from 'bpmn-engine'; +// eslint-disable-next-line no-duplicate-imports import Engine from 'bpmn-engine'; import * as factory from './helpers/factory.js'; import * as testHelpers from './helpers/testHelpers.js'; diff --git a/test/feature/bpmn-moddle-backward-compatibility-feature.js b/test/feature/bpmn-moddle-backward-compatibility-feature.js new file mode 100644 index 0000000..a4ac282 --- /dev/null +++ b/test/feature/bpmn-moddle-backward-compatibility-feature.js @@ -0,0 +1,97 @@ +import { EventEmitter } from 'node:events'; + +import { Engine } from 'bpmn-engine'; +import BpmnModdle9 from 'bpmn-moddle'; +import { BpmnModdle as BpmnModdle10 } from 'bpmn-moddle-10'; + +import * as factory from '../helpers/factory.js'; + +/** + * bpmn-moddle is a peer dependency with the range `>=9`, so a consumer may run + * the engine on either v9 or v10. v10 ships the parser as a *named* export + * (`import { BpmnModdle }`) instead of the v9 default export, but the moddle + * context it produces is identical. These features pin that compatibility down: + * state persisted by a deployment on v9 must keep resuming after an upgrade to v10. + */ +Feature('bpmn-moddle 9/10 backward compatibility', () => { + const source = factory.resource('mother-of-all.bpmn').toString(); + const services = { + serviceFn(...args) { + args.pop()(); + }, + }; + + Scenario('state saved on bpmn-moddle@9 is resumed on bpmn-moddle@10', () => { + let state; + Given('the mother-of-all source parsed with bpmn-moddle@9', () => { + expect(BpmnModdle9, 'v9 default export').to.be.a('function'); + }); + + let engine, stopped; + And('an engine started from that v9 moddle context', async () => { + const moddleContext = await new BpmnModdle9().fromXML(source.trim()); + engine = new Engine({ name: 'saved on v9', moddleContext, services }); + + const listener = new EventEmitter(); + listener.once('activity.wait', (_, engineApi) => engineApi.stop()); + + stopped = engine.waitFor('stop'); + engine.execute({ listener }); + }); + + And('it is stopped at the first user task wait', () => { + return stopped; + }); + + Then('engine state can be saved', async () => { + state = await engine.getState(); + expect(state.definitions[0]).to.have.property('id', 'Definitions_1'); + expect(state.definitions[0]).to.have.property('source').that.is.a('string'); + }); + + let slimmerState; + And('the persisted runtime state without the embedded v9 source', () => { + slimmerState = JSON.parse(JSON.stringify(state)); + slimmerState.definitions[0].source = undefined; + }); + + let recovered, ended; + When('a fresh engine parses the same source with bpmn-moddle@10 and recovers the state', async () => { + expect(BpmnModdle10, 'v10 named export').to.be.a('function'); + + const moddleContext = await new BpmnModdle10().fromXML(source.trim()); + recovered = new Engine({ name: 'resumed on v10', moddleContext, services }); + recovered.recover(slimmerState); + }); + + And('the recovered engine is resumed, signalling every wait', () => { + const listener = new EventEmitter(); + listener.on('activity.wait', (activityApi) => activityApi.signal()); + + ended = recovered.waitFor('end'); + recovered.resume({ listener }); + }); + + Then('execution completes', async () => { + await ended; + expect(recovered).to.have.property('state', 'idle'); + }); + }); + + Scenario('bpmn-moddle@9 and bpmn-moddle@10 serialize to the same source context', () => { + let v9Context, v10Context; + Given('the source serialized through an engine using bpmn-moddle@9', async () => { + const moddleContext = await new BpmnModdle9().fromXML(source.trim()); + v9Context = await new Engine({ moddleContext }).getDefinitions().then((defs) => defs[0].environment.options.source); + }); + + And('the source serialized through an engine using bpmn-moddle@10', async () => { + const moddleContext = await new BpmnModdle10().fromXML(source.trim()); + v10Context = await new Engine({ moddleContext }).getDefinitions().then((defs) => defs[0].environment.options.source); + }); + + Then('both serialized contexts are equal', () => { + expect(JSON.parse(v9Context.serialize())).to.deep.equal(JSON.parse(v10Context.serialize())); + }); + }); +}); diff --git a/test/feature/extend-feature.js b/test/feature/extend-feature.js index 96b25e0..5dc32c9 100644 --- a/test/feature/extend-feature.js +++ b/test/feature/extend-feature.js @@ -598,7 +598,7 @@ Feature('extending behaviour', () => { for (const extension of activity.behaviour.extensionElements.values) { switch (extension.$type) { case 'camunda:ExecutionListener': { - myExtensions.push(ExecutionListener(extension)); + if (extension.script) myExtensions.push(ExecutionListener(extension)); break; } } diff --git a/test/feature/issues-feature.js b/test/feature/issues-feature.js index e34cc7e..92fb82a 100644 --- a/test/feature/issues-feature.js +++ b/test/feature/issues-feature.js @@ -1105,11 +1105,11 @@ Feature('Issues', () => { let output; if (content.isMultiInstance) { - output = rawOutput.map(({ id, executionId /* eslint-disable-line no-unused-vars */, ...rest }) => { + output = rawOutput.map(({ id, executionId, ...rest }) => { return rest; }); } else { - const { id, executionId, ...rest } = rawOutput; // eslint-disable-line no-unused-vars + const { id, executionId, ...rest } = rawOutput; output = rest; } From 8d1b286c6c16c78e844bd817f6e849b1d3d94299 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sun, 2 Aug 2026 08:49:30 +0200 Subject: [PATCH 07/14] update c8@12 --- package.json | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 56c0f25..e03043c 100644 --- a/package.json +++ b/package.json @@ -32,13 +32,13 @@ ], "scripts": { "test": "mocha -p -R @bonniernews/hot-bev", - "posttest": "npm run lint && npm run toc && npm run dist", + "posttest": "npm run lint && npm run toc && npm run dist && npm run test:md", "lint": "eslint . --cache && prettier . -c --cache", "wintest": "mocha", "cov:html": "c8 -r html -r text mocha -p -R @bonniernews/hot-bev", "test:lcov": "c8 -r lcov mocha && npm run lint", "toc": "node scripts/generate-api-toc ./docs/API.md,./docs/Examples.md", - "test-md": "texample ./docs/API.md,./docs/Examples.md,./docs/Upgrade.md", + "test:md": "texample ./docs/API.md,./docs/Examples.md,./docs/Upgrade.md", "dist": "rollup -c", "prepack": "npm run dist" }, @@ -58,9 +58,6 @@ } ], "overrides": { - "c8": { - "yargs": "^18.0.0" - }, "mocha": { "yargs": "^18.0.0" } @@ -75,16 +72,16 @@ "bpmn-elements": "^18.0.0", "bpmn-moddle": "^9.0.4", "bpmn-moddle-10": "npm:bpmn-moddle@^10.0.0", - "c8": "^11.0.0", + "c8": "^12.0.0", "camunda-bpmn-moddle": "^7.0.1", "chai": "^6.2.0", "chronokinesis": "^8.0.0", "debug": "^4.4.3", "eslint": "^10.5.0", "globals": "^17.6.0", - "moddle-context-serializer": "^6.0.0", "mocha": "^11.0.1", "mocha-cakes-2": "^3.3.0", + "moddle-context-serializer": "^6.0.0", "prettier": "^3.2.5", "rollup": "^4.10.0", "texample": "^1.0.2" From 15a77c2bb34ee61f0fb6af67c9b76a36fc69bf1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sun, 2 Aug 2026 08:50:24 +0200 Subject: [PATCH 08/14] update gh actions packages --- .github/workflows/build.yaml | 4 ++-- .github/workflows/release.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 6a4651f..3dc7cf4 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,9 +14,9 @@ jobs: node-version: [20, 22, 24, latest] steps: - name: Checkout - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: ${{ matrix.node-version }} - run: npm i diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f42f20b..f84d1c7 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -13,9 +13,9 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - - uses: actions/setup-node@v6 + - uses: actions/setup-node@v7 with: node-version: '24' registry-url: 'https://registry.npmjs.org' From d424f4d92162c0dcfda4234d5568e6518589d447 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 15 Aug 2026 11:47:11 +0200 Subject: [PATCH 09/14] patch bpmn-elements --- .mocharc.json | 2 +- AGENTS.md | 2 +- CHANGELOG.md | 5 ++ eslint.config.js | 1 + lib/index.cjs | 28 +++---- package.json | 12 ++- src/index.js | 28 +++---- test/Engine-test.js | 12 ++- test/feature/Engine-feature.js | 27 +++++-- .../feature/backward-compatibility-feature.js | 1 + ...n-moddle-backward-compatibility-feature.js | 11 ++- test/feature/call-activity-feature.js | 3 + test/feature/conditional-event-feature.js | 4 +- test/feature/engine-output-feature.js | 2 + test/feature/extend-feature.js | 6 +- test/feature/issues-feature.js | 19 +++-- test/feature/issues/issue-163-feature.js | 5 +- test/feature/issues/issue-172-feature.js | 1 + test/feature/issues/issue-187-feature.js | 1 + test/feature/issues/issue-199-feature.js | 1 + test/feature/multiple-sources-feature.js | 30 +++---- test/feature/resume-feature.js | 8 +- test/feature/scripts-feature.js | 12 ++- test/feature/shake-feature.js | 5 +- test/feature/timers-feature.js | 9 ++- test/globals.d.ts | 16 ++++ test/helpers/factory.js | 4 + test/helpers/setup.js | 5 ++ test/helpers/testHelpers.js | 1 + test/issues-test.js | 2 +- test/resources/JsExtension.js | 6 +- test/tsconfig.json | 13 +++ tsconfig.json | 6 +- types/index.d.ts | 35 ++++---- types/interfaces.d.ts | 80 ------------------- 35 files changed, 221 insertions(+), 182 deletions(-) create mode 100644 test/globals.d.ts create mode 100644 test/helpers/setup.js create mode 100644 test/tsconfig.json delete mode 100644 types/interfaces.d.ts diff --git a/.mocharc.json b/.mocharc.json index 3a6a52d..767e288 100644 --- a/.mocharc.json +++ b/.mocharc.json @@ -4,5 +4,5 @@ "timeout": 1000, "ui": "mocha-cakes-2", "spec": ["test/**/*-feature.js", "test/**/*-test.js"], - "require": ["chai/register-expect.js"] + "require": ["chai/register-expect.js", "./test/helpers/setup.js"] } diff --git a/AGENTS.md b/AGENTS.md index e7f5a67..c327095 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -49,7 +49,7 @@ smqp (peer dep) ── message broker driving execution ## Build & publishing -- `type: module` in `package.json`; ESM is `src/index.js`, CJS is `lib/index.cjs` (rollup), types are hand-maintained at `types/index.d.ts` (the published `declare module 'bpmn-engine'` surface) with shared interface definitions in `types/interfaces.d.ts` (referenced by `src` JSDoc via the tsconfig `types` path). +- `type: module` in `package.json`; ESM is `src/index.js`, CJS is `lib/index.cjs` (rollup), types are hand-maintained at `types/index.d.ts` (the published `declare module 'bpmn-engine'` surface, referenced by `src` JSDoc via package self-reference `import('bpmn-engine')`). - `prepack` runs `dist`, so `npm publish` always ships a fresh CJS bundle. `lib/index.cjs` is tracked in git — regenerate (`npm run dist`) and commit alongside source changes that affect the public API. - Tests import via package self-reference (`import { Engine } from 'bpmn-engine'`), not `'../src/index.js'`. This exercises the same module resolution consumers use. diff --git a/CHANGELOG.md b/CHANGELOG.md index 42b839c..561aaeb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v26.0.2 - 2026-08-15 + +- fix type declarations: `typeResolver` option is a type resolver extender function, `JavaScripts` is callable without `new`, and `getScript` may return undefined +- type-check the test suite with the corrected `bpmn-elements@18` type declarations + ## v26.0.1 - 2026-07-07 - declare `bpmn-elements`, `bpmn-moddle`, `moddle-context-serializer`, and `debug` as peer dependencies (install and pin them yourself) diff --git a/eslint.config.js b/eslint.config.js index 0dd4557..0aaafa0 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -56,6 +56,7 @@ const rules = { ], 'no-var': 2, 'no-with': 2, + 'prefer-arrow-callback': ['error', { allowNamedFunctions: true }], 'prefer-const': ['error', { destructuring: 'all' }], 'require-atomic-updates': 0, 'require-await': 2, diff --git a/lib/index.cjs b/lib/index.cjs index b61029e..7763df8 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -177,7 +177,7 @@ const kTypeResolver = Symbol.for('type resolver'); /** * BPMN 2.0 execution engine. - * @param {import('types').BpmnEngineOptions} [options] + * @param {import('bpmn-engine').BpmnEngineOptions} [options] */ function Engine(options) { if (!(this instanceof Engine)) return new Engine(options); @@ -266,7 +266,7 @@ Object.defineProperties(Engine.prototype, { /** * Execute the loaded definitions. - * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {import('bpmn-engine').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] * @param {(err: Error, execution?: Execution) => void} [callback] * @returns {Promise} */ @@ -292,8 +292,8 @@ Engine.prototype.stop = function stop() { /** * Recover engine from saved state. - * @param {import('types').BpmnEngineExecutionState | null} savedState - * @param {import('types').BpmnEngineOptions} [recoverOptions] + * @param {import('bpmn-engine').BpmnEngineExecutionState | null} savedState + * @param {import('bpmn-engine').BpmnEngineOptions} [recoverOptions] * @returns {Engine} */ Engine.prototype.recover = function recover(savedState, recoverOptions) { @@ -343,7 +343,7 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { /** * Resume execution from a previously recovered state. - * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {import('bpmn-engine').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] * @param {(err: Error, execution?: Execution) => void} [callback] * @returns {Promise} */ @@ -370,7 +370,7 @@ Engine.prototype.resume = async function resume(...args) { /** * Add a pre-serialized source context to the engine. - * @param {{ sourceContext: import('types').SerializableContext }} [options] + * @param {{ sourceContext: import('moddle-context-serializer').SerializableContext }} [options] */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; @@ -380,7 +380,7 @@ Engine.prototype.addSource = function addSource(options) { }; /** - * @param {import('types').BpmnEngineExecuteOptions} [executeOptions] + * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [executeOptions] * @returns {Promise} */ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { @@ -397,7 +397,7 @@ Engine.prototype.getDefinitionById = async function getDefinitionById(id) { return (await this.getDefinitions()).find((d) => d.id === id); }; -/** @returns {Promise} */ +/** @returns {Promise} */ Engine.prototype.getState = async function getState() { const execution = this.execution; if (execution) return execution.getState(); @@ -408,7 +408,7 @@ Engine.prototype.getState = async function getState() { /** * @template R - * @param {import('types').BpmnEngineEvent} eventName + * @param {import('bpmn-engine').BpmnEngineEvent} eventName * @returns {Promise} */ Engine.prototype.waitFor = function waitFor(eventName) { @@ -478,7 +478,7 @@ Engine.prototype._getModdleContext = function getModdleContext(source) { /** * @param {Engine} engine * @param {any[]} definitions - * @param {import('types').BpmnEngineExecuteOptions} [options] + * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [options] * @param {boolean} [isRecovered] */ function Execution(engine, definitions, options, isRecovered = false) { @@ -755,7 +755,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; -/** @returns {import('types').BpmnEngineExecutionState} */ +/** @returns {import('bpmn-engine').BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -797,7 +797,7 @@ Execution.prototype.getPostponed = function getPostponed() { }; /** - * @param {import('types').BpmnMessage} [payload] + * @param {import('bpmn-engine').BpmnMessage} [payload] * @param {{ ignoreSameDefinition?: boolean }} [signalOptions] */ Execution.prototype.signal = function signal(payload, signalOptions) { @@ -807,7 +807,7 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; -/** @param {import('types').BpmnMessage} [payload] */ +/** @param {import('bpmn-engine').BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); @@ -816,7 +816,7 @@ Execution.prototype.cancelActivity = function cancelActivity(payload) { /** * @template T - * @param {import('types').BpmnEngineEvent} eventName + * @param {import('bpmn-engine').BpmnEngineEvent} eventName * @returns {Promise} */ Execution.prototype.waitFor = function waitFor(...args) { diff --git a/package.json b/package.json index e03043c..ac217ad 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bpmn-engine", "description": "BPMN 2.0 execution engine. Open source javascript workflow engine.", - "version": "26.0.1", + "version": "26.0.2", "type": "module", "module": "./src/index.js", "main": "./lib/index.cjs", @@ -38,7 +38,7 @@ "cov:html": "c8 -r html -r text mocha -p -R @bonniernews/hot-bev", "test:lcov": "c8 -r lcov mocha && npm run lint", "toc": "node scripts/generate-api-toc ./docs/API.md,./docs/Examples.md", - "test:md": "texample ./docs/API.md,./docs/Examples.md,./docs/Upgrade.md", + "test:md": "texample ./docs/API.md,./docs/Examples.md,./docs/Upgrade.md -c .mocharc.json", "dist": "rollup -c", "prepack": "npm run dist" }, @@ -67,9 +67,11 @@ "@eslint/js": "^10.0.1", "@rollup/plugin-commonjs": "^29.0.0", "@types/bpmn-moddle": "^10.0.0", + "@types/chai": "^5.2.3", + "@types/mocha": "^10.0.10", "@types/node": "^20.19.40", "bent": "^7.3.12", - "bpmn-elements": "^18.0.0", + "bpmn-elements": "^18.0.15", "bpmn-moddle": "^9.0.4", "bpmn-moddle-10": "npm:bpmn-moddle@^10.0.0", "c8": "^12.0.0", @@ -82,9 +84,11 @@ "mocha": "^11.0.1", "mocha-cakes-2": "^3.3.0", "moddle-context-serializer": "^6.0.0", + "nock": "^14.0.17", "prettier": "^3.2.5", "rollup": "^4.10.0", - "texample": "^1.0.2" + "texample": "^1.0.2", + "typescript": "^7.0.2" }, "peerDependencies": { "bpmn-elements": ">=18", diff --git a/src/index.js b/src/index.js index 3c53c4c..0367cec 100644 --- a/src/index.js +++ b/src/index.js @@ -31,7 +31,7 @@ export { JavaScripts }; /** * BPMN 2.0 execution engine. - * @param {import('types').BpmnEngineOptions} [options] + * @param {import('bpmn-engine').BpmnEngineOptions} [options] */ export function Engine(options) { if (!(this instanceof Engine)) return new Engine(options); @@ -120,7 +120,7 @@ Object.defineProperties(Engine.prototype, { /** * Execute the loaded definitions. - * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {import('bpmn-engine').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] * @param {(err: Error, execution?: Execution) => void} [callback] * @returns {Promise} */ @@ -146,8 +146,8 @@ Engine.prototype.stop = function stop() { /** * Recover engine from saved state. - * @param {import('types').BpmnEngineExecutionState | null} savedState - * @param {import('types').BpmnEngineOptions} [recoverOptions] + * @param {import('bpmn-engine').BpmnEngineExecutionState | null} savedState + * @param {import('bpmn-engine').BpmnEngineOptions} [recoverOptions] * @returns {Engine} */ Engine.prototype.recover = function recover(savedState, recoverOptions) { @@ -197,7 +197,7 @@ Engine.prototype.recover = function recover(savedState, recoverOptions) { /** * Resume execution from a previously recovered state. - * @param {import('types').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] + * @param {import('bpmn-engine').BpmnEngineExecuteOptions | ((err: Error, execution?: Execution) => void)} [optionsOrCallback] * @param {(err: Error, execution?: Execution) => void} [callback] * @returns {Promise} */ @@ -224,7 +224,7 @@ Engine.prototype.resume = async function resume(...args) { /** * Add a pre-serialized source context to the engine. - * @param {{ sourceContext: import('types').SerializableContext }} [options] + * @param {{ sourceContext: import('moddle-context-serializer').SerializableContext }} [options] */ Engine.prototype.addSource = function addSource(options) { if (!options?.sourceContext) return; @@ -234,7 +234,7 @@ Engine.prototype.addSource = function addSource(options) { }; /** - * @param {import('types').BpmnEngineExecuteOptions} [executeOptions] + * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [executeOptions] * @returns {Promise} */ Engine.prototype.getDefinitions = function getDefinitions(executeOptions) { @@ -251,7 +251,7 @@ Engine.prototype.getDefinitionById = async function getDefinitionById(id) { return (await this.getDefinitions()).find((d) => d.id === id); }; -/** @returns {Promise} */ +/** @returns {Promise} */ Engine.prototype.getState = async function getState() { const execution = this.execution; if (execution) return execution.getState(); @@ -262,7 +262,7 @@ Engine.prototype.getState = async function getState() { /** * @template R - * @param {import('types').BpmnEngineEvent} eventName + * @param {import('bpmn-engine').BpmnEngineEvent} eventName * @returns {Promise} */ Engine.prototype.waitFor = function waitFor(eventName) { @@ -332,7 +332,7 @@ Engine.prototype._getModdleContext = function getModdleContext(source) { /** * @param {Engine} engine * @param {any[]} definitions - * @param {import('types').BpmnEngineExecuteOptions} [options] + * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [options] * @param {boolean} [isRecovered] */ export function Execution(engine, definitions, options, isRecovered = false) { @@ -609,7 +609,7 @@ Execution.prototype._saveOutput = function saveOutput(output) { } }; -/** @returns {import('types').BpmnEngineExecutionState} */ +/** @returns {import('bpmn-engine').BpmnEngineExecutionState} */ Execution.prototype.getState = function getState() { const definitions = []; for (const definition of this.definitions) { @@ -651,7 +651,7 @@ Execution.prototype.getPostponed = function getPostponed() { }; /** - * @param {import('types').BpmnMessage} [payload] + * @param {import('bpmn-engine').BpmnMessage} [payload] * @param {{ ignoreSameDefinition?: boolean }} [signalOptions] */ Execution.prototype.signal = function signal(payload, signalOptions) { @@ -661,7 +661,7 @@ Execution.prototype.signal = function signal(payload, signalOptions) { } }; -/** @param {import('types').BpmnMessage} [payload] */ +/** @param {import('bpmn-engine').BpmnMessage} [payload] */ Execution.prototype.cancelActivity = function cancelActivity(payload) { for (const definition of this[kExecuting]) { definition.cancelActivity(payload); @@ -670,7 +670,7 @@ Execution.prototype.cancelActivity = function cancelActivity(payload) { /** * @template T - * @param {import('types').BpmnEngineEvent} eventName + * @param {import('bpmn-engine').BpmnEngineEvent} eventName * @returns {Promise} */ Execution.prototype.waitFor = function waitFor(...args) { diff --git a/test/Engine-test.js b/test/Engine-test.js index 92b3b9b..2e431e5 100644 --- a/test/Engine-test.js +++ b/test/Engine-test.js @@ -44,6 +44,7 @@ describe('Engine', () => { it('throws if unsupported source is passed', (done) => { const engine = Bpmn.Engine({ + // @ts-expect-error source: {}, }); @@ -66,6 +67,7 @@ describe('Engine', () => { it('but not function', (done) => { const engine = Bpmn.Engine({ + // @ts-expect-error source() {}, }); @@ -123,6 +125,7 @@ describe('Engine', () => { expect(() => Bpmn.Engine({ source: factory.valid(), + // @ts-expect-error Logger, }) ).to.throw(TypeError, 'Logger is not a function'); @@ -306,7 +309,7 @@ describe('Engine', () => { input: 0, }, services: { - get(context, next) { + get(_context, next) { next(new Error('Inner error')); }, }, @@ -433,7 +436,7 @@ describe('Engine', () => { name: 'end test', source, services: { - get: (context, next) => { + get: (_context, next) => { next(new Error('Inner error')); }, }, @@ -461,7 +464,7 @@ describe('Engine', () => { name: 'end test', source, services: { - get: (context, next) => { + get: (_context, next) => { next(new Error('Inner error')); }, }, @@ -482,6 +485,7 @@ describe('Engine', () => { try { await engine.execute({ + // @ts-expect-error listener: {}, }); } catch (e) { @@ -1156,6 +1160,7 @@ describe('Engine', () => { it('execution api returns activities in a postponed state', async () => { const listener = new EventEmitter(); + /** @type {import('bpmn-engine').Execution} */ let engineApi; listener.once('wait', (_, api) => { engineApi = api; @@ -1230,6 +1235,7 @@ describe('Engine', () => { sourceContext: updateContext, }); + /** @type {import('bpmn-engine').Execution} */ let engineApi; listener.once('wait', (_, api) => { engineApi = api; diff --git a/test/feature/Engine-feature.js b/test/feature/Engine-feature.js index 78a63ca..60dec87 100644 --- a/test/feature/Engine-feature.js +++ b/test/feature/Engine-feature.js @@ -5,7 +5,10 @@ import { camundaBpmnModdle as camunda } from '../helpers/testHelpers.js'; Feature('Engine', () => { Scenario('Mother of all', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Buffer} */ + let source; Given('a massive source with user task, sub process, lanes, and a loop back', () => { source = factory.resource('mother-of-all.bpmn'); }); @@ -126,7 +129,10 @@ Feature('Engine', () => { }); Scenario('A definition with lanes', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Buffer} */ + let source; Given('a bpmn source with two lanes with message flows', () => { source = factory.resource('lanes.bpmn'); }); @@ -167,7 +173,10 @@ Feature('Engine', () => { }); Scenario('Activity extension', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {string} */ + let source; Given('a bpmn source with user tasks', () => { source = ` { }); Scenario('Execute with callback', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Buffer} */ + let source; Given('a massive source with user task, timeouts, and the rest', () => { source = factory.resource('mother-of-all.bpmn'); }); @@ -316,7 +328,10 @@ Feature('Engine', () => { }); Scenario('Resume with callback', () => { - let engine, source; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Buffer} */ + let source; Given('a massive source with user task, timeouts, and the rest', () => { source = factory.resource('mother-of-all.bpmn'); }); @@ -336,7 +351,7 @@ Feature('Engine', () => { let listener; And('expects to be stopped at first user task wait', () => { listener = new EventEmitter(); - listener.once('activity.wait', (activityApi, engineApi) => { + listener.once('activity.wait', (_activityApi, engineApi) => { engineApi.stop(); }); }); diff --git a/test/feature/backward-compatibility-feature.js b/test/feature/backward-compatibility-feature.js index deb5a6f..84be4f0 100644 --- a/test/feature/backward-compatibility-feature.js +++ b/test/feature/backward-compatibility-feature.js @@ -86,6 +86,7 @@ Feature('Backward compatability', () => { delete state.definitions[0].execution.processes[0].environment; }); + /** @type {Engine} */ let recovered; When('engine is recovered with old state in latest version', () => { const upgradedState = upgradeStateToVersion14(state); diff --git a/test/feature/bpmn-moddle-backward-compatibility-feature.js b/test/feature/bpmn-moddle-backward-compatibility-feature.js index a4ac282..ce23a07 100644 --- a/test/feature/bpmn-moddle-backward-compatibility-feature.js +++ b/test/feature/bpmn-moddle-backward-compatibility-feature.js @@ -1,6 +1,7 @@ import { EventEmitter } from 'node:events'; import { Engine } from 'bpmn-engine'; +// @ts-ignore import BpmnModdle9 from 'bpmn-moddle'; import { BpmnModdle as BpmnModdle10 } from 'bpmn-moddle-10'; @@ -27,7 +28,10 @@ Feature('bpmn-moddle 9/10 backward compatibility', () => { expect(BpmnModdle9, 'v9 default export').to.be.a('function'); }); - let engine, stopped; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {Promise} */ + let stopped; And('an engine started from that v9 moddle context', async () => { const moddleContext = await new BpmnModdle9().fromXML(source.trim()); engine = new Engine({ name: 'saved on v9', moddleContext, services }); @@ -55,7 +59,10 @@ Feature('bpmn-moddle 9/10 backward compatibility', () => { slimmerState.definitions[0].source = undefined; }); - let recovered, ended; + /** @type {import('bpmn-engine').Engine} */ + let recovered; + /** @type {Promise} */ + let ended; When('a fresh engine parses the same source with bpmn-moddle@10 and recovers the state', async () => { expect(BpmnModdle10, 'v10 named export').to.be.a('function'); diff --git a/test/feature/call-activity-feature.js b/test/feature/call-activity-feature.js index 5f89c49..e26a7ce 100644 --- a/test/feature/call-activity-feature.js +++ b/test/feature/call-activity-feature.js @@ -108,6 +108,7 @@ Feature('Call activity', () => { }); Scenario('call activity is canceled', () => { + /** @type {Engine} */ let engine; Given('a process with a call activity referencing a process', () => { const source = ` @@ -201,6 +202,7 @@ Feature('Call activity', () => { }); Scenario('call activity is discarded mid run', () => { + /** @type {Engine} */ let engine; Given('a process with a call activity referencing a process', () => { const source = ` @@ -244,6 +246,7 @@ Feature('Call activity', () => { }); Scenario('a process with a parallel multi-instance call activity with cardinality of three', () => { + /** @type {Engine} */ let engine; const serviceCalls = []; Given('two processes', () => { diff --git a/test/feature/conditional-event-feature.js b/test/feature/conditional-event-feature.js index 8d99d2c..fd83e6e 100644 --- a/test/feature/conditional-event-feature.js +++ b/test/feature/conditional-event-feature.js @@ -5,7 +5,9 @@ const boundJsEventSource = factory.resource('conditional-bound-js-event.bpmn'); Feature('Conditional event', () => { Scenario('A service with conditional bound expression event', () => { - let source, engine, serviceCallback; + /** @type {Engine} */ + let engine; + let source, serviceCallback; Given('a source matching scenario', () => { source = ` diff --git a/test/feature/engine-output-feature.js b/test/feature/engine-output-feature.js index 4a0605c..a2b32cb 100644 --- a/test/feature/engine-output-feature.js +++ b/test/feature/engine-output-feature.js @@ -2,6 +2,7 @@ import { Engine } from 'bpmn-engine'; Feature('Engine output', () => { Scenario('Process completes with output', () => { + /** @type {Engine} */ let engine; Given('a process with output', () => { const source = ` @@ -77,6 +78,7 @@ Feature('Engine output', () => { }); Scenario('process fails', () => { + /** @type {Engine} */ let engine; Given('a process with output', () => { const source = ` diff --git a/test/feature/extend-feature.js b/test/feature/extend-feature.js index 5dc32c9..722cca0 100644 --- a/test/feature/extend-feature.js +++ b/test/feature/extend-feature.js @@ -149,7 +149,7 @@ Feature('extending behaviour', () => { camunda, }, services: { - serviceFn(scope, callback) { + serviceFn(_scope, callback) { callback(null, { data: 1 }); }, }, @@ -223,7 +223,7 @@ Feature('extending behaviour', () => { source, scripts: { register() {}, - getScript(scriptType, activity) { + getScript(_scriptType, activity) { if (activity.id === 'task1') { return { execute(scope, next) { @@ -621,7 +621,7 @@ Feature('extending behaviour', () => { broker.subscribeTmp( 'event', `activity.${extension.event}`, - (routingKey, message) => { + (_routingKey, message) => { script.execute(message); }, { noAck: true, consumerTag: '_my-extension' } diff --git a/test/feature/issues-feature.js b/test/feature/issues-feature.js index 92fb82a..31ac779 100644 --- a/test/feature/issues-feature.js +++ b/test/feature/issues-feature.js @@ -118,7 +118,7 @@ Feature('Issues', () => { name: 'issue 105', source: source1, services: { - async doTask1(scope, callback) { + async doTask1(_scope, callback) { await sleep(50); // calling other heavy service... return callback(null); }, @@ -355,7 +355,7 @@ Feature('Issues', () => { name: 'issue 105', source: source2, services: { - async doTask1(scope, callback) { + async doTask1(_scope, callback) { await sleep(50); // calling other heavy service... return callback(null); }, @@ -1321,15 +1321,22 @@ function TimersWithoutScope(options) { } function registerTimeout(owner) { - return function registeredSetTimeout(...args) { - return timersApi.setTimeout.call(owner, ...args); + return function registeredSetTimeout(callback, delay, ...args) { + return timersApi.setTimeout.call(owner, callback, delay, ...args); }; } + /** @this {any} */ function wrappedSetTimeout(callback, delay, ...args) { - const ref = { timerId: `timer_${count++}`, callback, delay, args, owner: this }; + const ref = { + timerId: `timer_${count++}`, + callback, + delay, + args, + owner: this, + timerRef: options.setTimeout.call(null, onTimeout, delay, ...args), + }; executing.push(ref); - ref.timerRef = options.setTimeout.call(null, onTimeout, delay, ...args); return ref; function onTimeout(...rargs) { diff --git a/test/feature/issues/issue-163-feature.js b/test/feature/issues/issue-163-feature.js index d388496..2450dc8 100644 --- a/test/feature/issues/issue-163-feature.js +++ b/test/feature/issues/issue-163-feature.js @@ -3,7 +3,10 @@ import * as factory from '../../helpers/factory.js'; Feature('issue 163', () => { Scenario('Get sub process postponed', () => { - let engine, execution; + /** @type {import('bpmn-engine').Engine} */ + let engine; + /** @type {import('bpmn-engine').Execution} */ + let execution; When('one usertask and two user tasks in sub process and finally a user task', async () => { const source = factory.resource('issue-163.bpmn'); diff --git a/test/feature/issues/issue-172-feature.js b/test/feature/issues/issue-172-feature.js index f924ab4..9531f1e 100644 --- a/test/feature/issues/issue-172-feature.js +++ b/test/feature/issues/issue-172-feature.js @@ -2,6 +2,7 @@ import { Engine } from 'bpmn-engine'; Feature('issue 172', () => { Scenario('Postpone intermediate throw event by formatting', () => { + /** @type {import('bpmn-engine').Engine} */ let engine; const agiCalls = []; Given('start event, intermediate throw event, and end event', () => { diff --git a/test/feature/issues/issue-187-feature.js b/test/feature/issues/issue-187-feature.js index 269135e..1cedd56 100644 --- a/test/feature/issues/issue-187-feature.js +++ b/test/feature/issues/issue-187-feature.js @@ -6,6 +6,7 @@ const source = factory.resource('issue-187.bpmn'); Feature('issue 187 - Complete condition in multi-instance loop with custom expression parser', () => { Scenario('user task is looped over list of handlers', () => { + /** @type {import('bpmn-engine').Engine} */ let engine; Given('start event, intermediate throw event, and end event', () => { engine = getEngine187(); diff --git a/test/feature/issues/issue-199-feature.js b/test/feature/issues/issue-199-feature.js index 1689c31..fcacf60 100644 --- a/test/feature/issues/issue-199-feature.js +++ b/test/feature/issues/issue-199-feature.js @@ -36,6 +36,7 @@ next(); Feature('issue 199 - Issue with Script Tasks After State Recovery in bpmn-engine', () => { Scenario('execute, recover resume with same engine instance', () => { + /** @type {import('bpmn-engine').Engine} */ let engine; Given('an engine with user task flanked by two script tasks', () => { engine = new Engine({ diff --git a/test/feature/multiple-sources-feature.js b/test/feature/multiple-sources-feature.js index 3ea7d42..3064ce7 100644 --- a/test/feature/multiple-sources-feature.js +++ b/test/feature/multiple-sources-feature.js @@ -6,6 +6,18 @@ import { camundaBpmnModdle as camunda } from '../helpers/testHelpers.js'; const signalsSource = factory.resource('signals.bpmn'); const sendSignalSource = factory.resource('send-signal.bpmn'); +class DataStores { + constructor(data) { + this.data = data; + } + getDataStore(id) { + return this.data[id]; + } + setDataStore(id, value) { + this.data[id] = value; + } +} + Feature('Multiple sources', () => { Scenario('Two definitions that communicates with signals', () => { let engine, signalContext, updateContext; @@ -687,7 +699,7 @@ function getExtendedEngine(options) { engine.broker.subscribeTmp( 'event', 'activity.signal', - (routingKey, msg) => { + (_routingKey, msg) => { engine.execution.signal(msg.content.message, { ignoreSameDefinition: true }); }, { noAck: true } @@ -713,7 +725,7 @@ function ServiceExpression(activity) { } } -function formFormatting(activity, context, formData) { +function formFormatting(activity, _context, formData) { const { broker, environment } = activity; broker.subscribeTmp( 'event', @@ -732,7 +744,7 @@ function formFormatting(activity, context, formData) { ); } -function ioFormatting(activity, context, ioData) { +function ioFormatting(activity, _context, ioData) { const { broker, environment } = activity; if (ioData.inputParameters) { broker.subscribeTmp( @@ -766,15 +778,3 @@ function ioFormatting(activity, context, ioData) { ); } } - -function DataStores(data) { - this.data = data; -} - -DataStores.prototype.getDataStore = function getDataStore(id) { - return this.data[id]; -}; - -DataStores.prototype.setDataStore = function setDataStore(id, value) { - this.data[id] = value; -}; diff --git a/test/feature/resume-feature.js b/test/feature/resume-feature.js index ba3e052..5feafdc 100644 --- a/test/feature/resume-feature.js +++ b/test/feature/resume-feature.js @@ -4,7 +4,9 @@ import { Engine } from 'bpmn-engine'; Feature('Resume execution', () => { Scenario('Execution is stopped and resumed', () => { - let engine, listener, source; + /** @type {Engine} */ + let engine; + let listener, source; Given('a bpmn source with a sub processes with one user task', () => { source = ` @@ -280,7 +282,9 @@ Feature('Resume execution', () => { }); Scenario('recover with options', () => { - let engine, listener, source; + /** @type {Engine} */ + let engine; + let listener, source; Given('a bpmn source with user task, timer and script', () => { source = ` diff --git a/test/feature/scripts-feature.js b/test/feature/scripts-feature.js index f4478d7..85ae0a3 100644 --- a/test/feature/scripts-feature.js +++ b/test/feature/scripts-feature.js @@ -2,7 +2,9 @@ import { Engine } from 'bpmn-engine'; Feature('Scripts', () => { Scenario('Process with scripts', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a bpmn source with empty script task', () => { source = ` { }); Scenario('Process with setTimeout in inline scripts task', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a bpmn source with script task with setTimeout', () => { source = ` { }); Scenario('Process with long running timer can be stopped', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a bpmn source with script task with setTimeout', () => { source = ` { Scenario('Determine run sequences for an activity', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a bpmn source with user tasks', () => { source = ` { `; }); + /** @type {import('bpmn-elements').Definition} */ let definition; And('an engine loaded with extension for fetching form and saving output', async () => { engine = Engine({ diff --git a/test/feature/timers-feature.js b/test/feature/timers-feature.js index 1fbf1a1..b8577c9 100644 --- a/test/feature/timers-feature.js +++ b/test/feature/timers-feature.js @@ -16,6 +16,7 @@ Feature('Timers', () => { }); after(ck.reset); + /** @type {Engine} */ let engine; Given('a time cycle start event, bound time duration event, throw time date event, and a user task with due date', async () => { const sourceContext = await testHelpers.context(timersSource, { @@ -244,7 +245,9 @@ Feature('Timers', () => { }); Scenario('engine with added arbitrary timers', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a source with user task and a bound timer event', () => { source = ` @@ -357,7 +360,9 @@ Feature('Timers', () => { }); Scenario('engine with invalid timers', () => { - let engine, source; + /** @type {Engine} */ + let engine; + let source; Given('a source with user task and a bound timer event with invalid date', () => { source = ` diff --git a/test/globals.d.ts b/test/globals.d.ts new file mode 100644 index 0000000..8cbd646 --- /dev/null +++ b/test/globals.d.ts @@ -0,0 +1,16 @@ +declare global { + const expect: Chai.ExpectStatic; + + // mocha-cakes-2 + const Feature: Mocha.SuiteFunction; + const Scenario: Mocha.SuiteFunction; + const Given: Mocha.TestFunction; + const When: Mocha.TestFunction; + const Then: Mocha.TestFunction; + const And: Mocha.TestFunction; + const But: Mocha.TestFunction; + const afterEachScenario: Mocha.HookFunction; + const beforeEachScenario: Mocha.HookFunction; +} + +export {}; diff --git a/test/helpers/factory.js b/test/helpers/factory.js index 0b694c9..81a54ec 100644 --- a/test/helpers/factory.js +++ b/test/helpers/factory.js @@ -83,6 +83,10 @@ export function multipleInbound() { `; } +/** + * @param {string} name + * @returns {Buffer} + */ export function resource(name) { return fs.readFileSync(path.join('./test/resources', name)); } diff --git a/test/helpers/setup.js b/test/helpers/setup.js new file mode 100644 index 0000000..74e2dce --- /dev/null +++ b/test/helpers/setup.js @@ -0,0 +1,5 @@ +import nock from 'nock'; + +process.env.NODE_ENV = 'test'; + +nock.enableNetConnect(/localhost|127\.0\.0\.1/); diff --git a/test/helpers/testHelpers.js b/test/helpers/testHelpers.js index 03e2596..0a5262d 100644 --- a/test/helpers/testHelpers.js +++ b/test/helpers/testHelpers.js @@ -1,6 +1,7 @@ import { createRequire } from 'node:module'; import { fileURLToPath } from 'node:url'; +// @ts-ignore import BpmnModdle from 'bpmn-moddle'; import * as Elements from 'bpmn-elements'; import Logger from '../../src/Logger.js'; diff --git a/test/issues-test.js b/test/issues-test.js index 0c92e7e..24620b3 100644 --- a/test/issues-test.js +++ b/test/issues-test.js @@ -24,7 +24,7 @@ describe('issues', () => { const states = []; - listener.on('activity.start', (activity, engineApi) => { + listener.on('activity.start', (_activity, engineApi) => { states.push(engineApi.getState()); }); diff --git a/test/resources/JsExtension.js b/test/resources/JsExtension.js index b3dd3be..f00e64c 100644 --- a/test/resources/JsExtension.js +++ b/test/resources/JsExtension.js @@ -13,9 +13,9 @@ export function extension(activity, context) { return { type: 'js:extension', extensions: { resultVariable, formKey }, - activate(msg) { - if (resultVariable) resultVariable.activate(msg); - if (formKey) formKey.activate(msg); + activate() { + if (resultVariable) resultVariable.activate(); + if (formKey) formKey.activate(); }, deactivate() { if (resultVariable) resultVariable.deactivate(); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..00678a7 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../tsconfig.json", + "include": ["**/*"], + "compilerOptions": { + "rootDir": "..", + "checkJs": true, + "emitDeclarationOnly": false, + "declaration": false, + "noEmit": true, + "noImplicitAny": false, + "types": ["node", "chai", "mocha"] + } +} diff --git a/tsconfig.json b/tsconfig.json index 115d6b8..454309b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,5 @@ { - "include": ["src/**/*", "types/interfaces.d.ts"], + "include": ["src/**/*", "types/index.d.ts"], "compilerOptions": { "noEmit": true, "rootDir": "src", @@ -18,8 +18,6 @@ "noImplicitThis": true, "noUnusedLocals": true, "noUnusedParameters": true, - "paths": { - "types": ["./types/interfaces.d.ts"] - } + "types": ["node"] } } diff --git a/types/index.d.ts b/types/index.d.ts index 935a6cd..6ca37e3 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -2,7 +2,7 @@ declare module 'bpmn-engine' { import type { EventEmitter } from 'node:events'; import type { Broker } from 'smqp'; import type { Definitions as BpmnModdleDefinitions } from 'bpmn-moddle'; - import type { ExtendFn, SerializableContext, ResolverFn } from 'moddle-context-serializer'; + import type { ExtendFn, SerializableContext, TypeResolverExtender } from 'moddle-context-serializer'; import type { ActivityStatus, Definition, @@ -16,7 +16,7 @@ declare module 'bpmn-engine' { Script, } from 'bpmn-elements'; - export type BpmnEngineEvent = 'error' | 'stop' | 'end'; + export type BpmnEngineEvent = 'error' | 'stop' | 'end' | 'wait'; export type BpmnActivityEvent = | 'activity.enter' @@ -49,16 +49,17 @@ declare module 'bpmn-engine' { export interface BpmnEngineOptions extends BpmnEngineExecuteOptions { name?: string; - source?: string; + source?: string | Buffer; sourceContext?: SerializableContext; elements?: Record; - typeResolver?: ResolverFn; + typeResolver?: TypeResolverExtender; extendFn?: ExtendFn; moddleOptions?: any; moddleContext?: BpmnModdleDefinitions; Logger?: (scope: string) => ILogger; scripts?: IScripts; disableDummyScript?: boolean; + listener?: EventEmitter | IListenerEmitter; [x: string]: any; } @@ -69,19 +70,19 @@ declare module 'bpmn-engine' { } export interface BpmnEngineExecutionState { - name: string; - engineVersion: string; - state: BpmnEngineRunningStatus; - stopped: boolean; - environment: EnvironmentState; - definitions: BpmnEngineDefinitionState[]; + name?: string; + engineVersion?: string; + state?: BpmnEngineRunningStatus; + stopped?: boolean; + environment?: EnvironmentState; + definitions?: BpmnEngineDefinitionState[]; } export function Engine(options?: BpmnEngineOptions): Engine; export class Engine extends EventEmitter { constructor(options?: BpmnEngineOptions); options: BpmnEngineOptions; - readonly name: string; + name: string; readonly broker: Broker; readonly logger: ILogger; readonly environment: Environment; @@ -99,7 +100,7 @@ declare module 'bpmn-engine' { getDefinitions(executeOptions?: BpmnEngineExecuteOptions): Promise; getState(): Promise; - recover(savedState: BpmnEngineExecutionState | null, recoverOptions?: BpmnEngineOptions): Engine; + recover(savedState?: BpmnEngineExecutionState | null, recoverOptions?: BpmnEngineOptions): Engine; resume(): Promise; resume(options: BpmnEngineExecuteOptions): Promise; @@ -143,11 +144,15 @@ declare module 'bpmn-engine' { waitFor(eventName: BpmnEngineEvent): Promise; } - export class JavaScripts implements IScripts { - constructor(disableDummy?: boolean); + export interface JavaScripts extends IScripts { register(activity: any): Script | undefined; - getScript(language: string, identifier: { id: string; [x: string]: any }): Script; + getScript(language: string, identifier: { id: string; [x: string]: any }): Script | undefined; } + export const JavaScripts: { + new (disableDummy?: boolean): JavaScripts; + /** callable without new */ + (disableDummy?: boolean): JavaScripts; + }; export default Engine; } diff --git a/types/interfaces.d.ts b/types/interfaces.d.ts deleted file mode 100644 index ee93701..0000000 --- a/types/interfaces.d.ts +++ /dev/null @@ -1,80 +0,0 @@ -import type { EventEmitter } from 'node:events'; -import type { Definitions as BpmnModdleDefinitions } from 'bpmn-moddle'; -import type { ExtendFn, SerializableContext, ResolverFn } from 'moddle-context-serializer'; -import type { ActivityStatus, ElementBroker, EnvironmentOptions, EnvironmentState, IScripts, Script, ILogger } from 'bpmn-elements'; - -export type { SerializableContext }; - -export type BpmnEngineEvent = 'error' | 'stop' | 'end'; - -export type BpmnActivityEvent = - | 'activity.enter' - | 'activity.start' - | 'activity.wait' - | 'wait' - | 'activity.end' - | 'activity.leave' - | 'activity.stop' - | 'activity.throw' - | 'activity.error'; - -export type BpmnSequenceFlowEvent = 'flow.take' | 'flow.discard' | 'flow.looped'; - -export type BpmnEngineRunningStatus = 'idle' | 'running' | 'stopped' | 'error'; - -export interface BpmnMessage { - id?: string; - executionId?: string; - [name: string]: any; -} - -export interface IListenerEmitter { - emit(eventName: string, ...args: any[]): void; -} - -export interface BpmnEngineExecuteOptions extends EnvironmentOptions { - listener?: EventEmitter | IListenerEmitter; -} - -export interface BpmnEngineOptions extends BpmnEngineExecuteOptions { - name?: string; - source?: string; - sourceContext?: SerializableContext; - elements?: Record; - typeResolver?: ResolverFn; - extendFn?: ExtendFn; - moddleOptions?: any; - moddleContext?: BpmnModdleDefinitions; - Logger?: (scope: string) => ILogger; - scripts?: IScripts; - disableDummyScript?: boolean; - [x: string]: any; -} - -export interface BpmnEngineDefinitionState { - state: string; - source?: string; - [x: string]: any; -} - -export interface BpmnEngineExecutionState { - name: string; - engineVersion: string; - state: BpmnEngineRunningStatus; - stopped: boolean; - environment: EnvironmentState; - definitions: BpmnEngineDefinitionState[]; -} - -export type { - ActivityStatus, - BpmnModdleDefinitions, - ElementBroker, - EnvironmentOptions, - EnvironmentState, - ExtendFn, - ILogger, - IScripts, - ResolverFn, - Script, -}; From ac29618aeaa5b627706757e66e12b81f1ffd1f6f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 22 Aug 2026 06:52:03 +0200 Subject: [PATCH 10/14] support bpmn-moddle >= 9 as promised --- CHANGELOG.md | 4 +++ docs/bpmn-moddle.md | 13 +++----- lib/index.cjs | 10 ++++-- package.json | 2 +- src/index.js | 9 ++++-- ...n-moddle-backward-compatibility-feature.js | 26 ++++++++++++++++ test/helpers/bpmn-moddle-10/hooks.js | 9 ++++++ test/helpers/bpmn-moddle-10/register.js | 3 ++ .../bpmn-moddle-10/run-engine-source.js | 31 +++++++++++++++++++ 9 files changed, 91 insertions(+), 16 deletions(-) create mode 100644 test/helpers/bpmn-moddle-10/hooks.js create mode 100644 test/helpers/bpmn-moddle-10/register.js create mode 100644 test/helpers/bpmn-moddle-10/run-engine-source.js diff --git a/CHANGELOG.md b/CHANGELOG.md index 561aaeb..d3ea87e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v26.0.3 - 2026-08-22 + +- support both `bpmn-moddle` 9 and 10 as promised by package + ## v26.0.2 - 2026-08-15 - fix type declarations: `typeResolver` option is a type resolver extender function, `JavaScripts` is callable without `new`, and `getScript` may return undefined diff --git a/docs/bpmn-moddle.md b/docs/bpmn-moddle.md index 75a4354..e901fd8 100644 --- a/docs/bpmn-moddle.md +++ b/docs/bpmn-moddle.md @@ -27,15 +27,9 @@ import BpmnModdle from 'bpmn-moddle'; import { BpmnModdle } from 'bpmn-moddle'; ``` -`bpmn-engine`'s own source still imports the v9 default export (`import BpmnModdle from 'bpmn-moddle'`). If you let the engine parse XML for you (the `source` option, or `Engine.prototype._getModdleContext`), stay on the 9.x line. To run on 10.x, parse the XML yourself with the named export and pass the result to the engine via the `moddleContext` option: +`bpmn-engine` resolves whichever export shape the installed version provides, so the `source` option — where the engine parses the XML for you — works on both lines. No code changes are needed to move between 9 and 10. -```js -import { Engine } from 'bpmn-engine'; -import { BpmnModdle } from 'bpmn-moddle'; // v10 - -const moddleContext = await new BpmnModdle().fromXML(source); -const engine = new Engine({ name: 'on v10', moddleContext }); -``` +One caveat for CommonJS consumers (`require('bpmn-engine')`): bpmn-moddle 10 is ESM-only, so the engine's CJS bundle reaches it through Node's `require(esm)` support — available since Node 20.19 / 22.12. On older Node, stay on bpmn-moddle 9 or migrate to `import`. ## Backward compatibility of persisted state @@ -45,4 +39,5 @@ This is pinned down by the feature test [`test/feature/bpmn-moddle-backward-comp 1. starts the _mother-of-all_ process from a context parsed with bpmn-moddle **9**, stops it at the first user task wait and saves the state; 2. re-parses the same source with bpmn-moddle **10**, recovers the v9 runtime state against the v10 source context, and resumes it to completion; -3. asserts that the source context serialized via 9 and via 10 are byte-for-byte equal. +3. asserts that the source context serialized via 9 and via 10 are byte-for-byte equal; +4. runs the engine with the raw `source` option in a child process where `bpmn-moddle` resolves to **10**, proving the engine's own parse path handles the named export. diff --git a/lib/index.cjs b/lib/index.cjs index 7763df8..3426256 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -5,7 +5,7 @@ Object.defineProperty(exports, '__esModule', { value: true }); var node_module = require('node:module'); var node_events = require('node:events'); var node_url = require('node:url'); -var BpmnModdle = require('bpmn-moddle'); +var bpmnModdle = require('bpmn-moddle'); var Elements = require('bpmn-elements'); var smqp = require('smqp'); var moddleContextSerializer = require('moddle-context-serializer'); @@ -30,6 +30,7 @@ function _interopNamespaceDefault(e) { return Object.freeze(n); } +var bpmnModdle__namespace = /*#__PURE__*/_interopNamespaceDefault(bpmnModdle); var Elements__namespace = /*#__PURE__*/_interopNamespaceDefault(Elements); function Logger(scope) { @@ -164,6 +165,9 @@ ProcessOutputDataObject.prototype.write = function writeDataObject(broker, excha const nodeRequire = node_module.createRequire(node_url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.tagName.toUpperCase() === 'SCRIPT' && _documentCurrentScript.src || new URL('index.cjs', document.baseURI).href)))); const { version: engineVersion } = nodeRequire('../package.json'); +// bpmn-moddle@9 exposes the parser as default export, bpmn-moddle@10 as the named export BpmnModdle +const BpmnModdle = bpmnModdle__namespace.BpmnModdle ?? bpmnModdle__namespace.default; + const kEngine = Symbol.for('engine'); const kEnvironment = Symbol.for('environment'); const kExecuting = Symbol.for('executing'); @@ -471,8 +475,8 @@ Engine.prototype._serializeModdleContext = function serializeModdleContext(moddl /** @internal */ Engine.prototype._getModdleContext = function getModdleContext(source) { - const bpmnModdle = new BpmnModdle(this.options.moddleOptions); - return bpmnModdle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); + const moddle = new BpmnModdle(this.options.moddleOptions); + return moddle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); }; /** diff --git a/package.json b/package.json index ac217ad..928200e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bpmn-engine", "description": "BPMN 2.0 execution engine. Open source javascript workflow engine.", - "version": "26.0.2", + "version": "26.0.3", "type": "module", "module": "./src/index.js", "main": "./lib/index.cjs", diff --git a/src/index.js b/src/index.js index 0367cec..99f3d97 100644 --- a/src/index.js +++ b/src/index.js @@ -2,7 +2,7 @@ import { createRequire } from 'node:module'; import { EventEmitter } from 'node:events'; import { fileURLToPath } from 'node:url'; -import BpmnModdle from 'bpmn-moddle'; +import * as bpmnModdle from 'bpmn-moddle'; import * as Elements from 'bpmn-elements'; import { Broker } from 'smqp'; import { Serializer, deserialize, TypeResolver } from 'moddle-context-serializer'; @@ -15,6 +15,9 @@ import ProcessOutputDataObject from './extensions/ProcessOutputDataObject.js'; const nodeRequire = createRequire(fileURLToPath(import.meta.url)); const { version: engineVersion } = nodeRequire('../package.json'); +// bpmn-moddle@9 exposes the parser as default export, bpmn-moddle@10 as the named export BpmnModdle +const BpmnModdle = bpmnModdle.BpmnModdle ?? bpmnModdle.default; + const kEngine = Symbol.for('engine'); const kEnvironment = Symbol.for('environment'); const kExecuting = Symbol.for('executing'); @@ -325,8 +328,8 @@ Engine.prototype._serializeModdleContext = function serializeModdleContext(moddl /** @internal */ Engine.prototype._getModdleContext = function getModdleContext(source) { - const bpmnModdle = new BpmnModdle(this.options.moddleOptions); - return bpmnModdle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); + const moddle = new BpmnModdle(this.options.moddleOptions); + return moddle.fromXML(Buffer.isBuffer(source) ? source.toString() : source.trim()); }; /** diff --git a/test/feature/bpmn-moddle-backward-compatibility-feature.js b/test/feature/bpmn-moddle-backward-compatibility-feature.js index ce23a07..7efde43 100644 --- a/test/feature/bpmn-moddle-backward-compatibility-feature.js +++ b/test/feature/bpmn-moddle-backward-compatibility-feature.js @@ -1,4 +1,6 @@ +import { execFile } from 'node:child_process'; import { EventEmitter } from 'node:events'; +import { promisify } from 'node:util'; import { Engine } from 'bpmn-engine'; // @ts-ignore @@ -101,4 +103,28 @@ Feature('bpmn-moddle 9/10 backward compatibility', () => { expect(JSON.parse(v9Context.serialize())).to.deep.equal(JSON.parse(v10Context.serialize())); }); }); + + Scenario('the engine parses raw source itself regardless of bpmn-moddle version', () => { + Given('the engine parses the source option with bpmn-moddle@9, the installed peer', async () => { + const definitions = await new Engine({ source }).getDefinitions(); + expect(definitions[0]).to.have.property('id', 'Definitions_1'); + }); + + let v10Run; + When('a child process where bpmn-moddle resolves to v10 runs the engine with the source option', async function whenSpawned() { + this.timeout(5000); + const helperDir = new URL('../helpers/bpmn-moddle-10/', import.meta.url); + const { stdout } = await promisify(execFile)(process.execPath, [ + '--import', + new URL('./register.js', helperDir).pathname, + new URL('./run-engine-source.js', helperDir).pathname, + ]); + v10Run = JSON.parse(stdout); + }); + + Then('the child process resolved the v10 named export and the engine parsed the source', () => { + expect(v10Run.resolvedModdleExports, 'v10 exports').to.deep.equal(['BpmnModdle']); + expect(v10Run).to.have.property('definitionId', 'Definitions_1'); + }); + }); }); diff --git a/test/helpers/bpmn-moddle-10/hooks.js b/test/helpers/bpmn-moddle-10/hooks.js new file mode 100644 index 0000000..7b0b2c3 --- /dev/null +++ b/test/helpers/bpmn-moddle-10/hooks.js @@ -0,0 +1,9 @@ +/** + * Module resolve hook that redirects every `bpmn-moddle` specifier to the + * `bpmn-moddle-10` npm alias (bpmn-moddle@^10), so a child process can run + * the engine as if bpmn-moddle 10 were the installed peer dependency. + */ +export function resolve(specifier, context, nextResolve) { + if (specifier === 'bpmn-moddle') return nextResolve('bpmn-moddle-10', context); + return nextResolve(specifier, context); +} diff --git a/test/helpers/bpmn-moddle-10/register.js b/test/helpers/bpmn-moddle-10/register.js new file mode 100644 index 0000000..19d345c --- /dev/null +++ b/test/helpers/bpmn-moddle-10/register.js @@ -0,0 +1,3 @@ +import { register } from 'node:module'; + +register(new URL('./hooks.js', import.meta.url)); diff --git a/test/helpers/bpmn-moddle-10/run-engine-source.js b/test/helpers/bpmn-moddle-10/run-engine-source.js new file mode 100644 index 0000000..98feb4a --- /dev/null +++ b/test/helpers/bpmn-moddle-10/run-engine-source.js @@ -0,0 +1,31 @@ +/** + * Runs the engine with the raw `source` option — the path that makes the + * engine itself import and instantiate bpmn-moddle. Launched by the + * bpmn-moddle backward compatibility feature with `--import ./register.js`, + * so `bpmn-moddle` resolves to bpmn-moddle@10 in this process. + * + * Prints a JSON verdict on stdout and exits non-zero on failure. + */ +import * as bpmnModdle from 'bpmn-moddle'; +import { Engine } from 'bpmn-engine'; + +import * as factory from '../factory.js'; + +const engine = new Engine({ + name: 'source parsed by the engine on bpmn-moddle@10', + source: factory.resource('mother-of-all.bpmn').toString(), + services: { + serviceFn(...args) { + args.pop()(); + }, + }, +}); + +const definitions = await engine.getDefinitions(); + +process.stdout.write( + JSON.stringify({ + resolvedModdleExports: Object.keys(bpmnModdle), + definitionId: definitions[0].id, + }) +); From cdd098aed4dc00bf4df656a835beb5b7cf1467e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Sat, 22 Aug 2026 07:02:40 +0200 Subject: [PATCH 11/14] make tests succeed on windows --- test/feature/bpmn-moddle-backward-compatibility-feature.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/feature/bpmn-moddle-backward-compatibility-feature.js b/test/feature/bpmn-moddle-backward-compatibility-feature.js index 7efde43..244fd14 100644 --- a/test/feature/bpmn-moddle-backward-compatibility-feature.js +++ b/test/feature/bpmn-moddle-backward-compatibility-feature.js @@ -1,5 +1,6 @@ import { execFile } from 'node:child_process'; import { EventEmitter } from 'node:events'; +import { fileURLToPath } from 'node:url'; import { promisify } from 'node:util'; import { Engine } from 'bpmn-engine'; @@ -116,8 +117,8 @@ Feature('bpmn-moddle 9/10 backward compatibility', () => { const helperDir = new URL('../helpers/bpmn-moddle-10/', import.meta.url); const { stdout } = await promisify(execFile)(process.execPath, [ '--import', - new URL('./register.js', helperDir).pathname, - new URL('./run-engine-source.js', helperDir).pathname, + new URL('./register.js', helperDir).href, + fileURLToPath(new URL('./run-engine-source.js', helperDir)), ]); v10Run = JSON.parse(stdout); }); From be9ed025a75edbf8867703bff383d3f64ca3640e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Tue, 8 Sep 2026 07:11:05 +0200 Subject: [PATCH 12/14] allow peer dep smqp@14 and type fixes --- CHANGELOG.md | 10 ++++++ docs/API.md | 10 ++++-- lib/index.cjs | 4 +-- package.json | 6 ++-- src/index.js | 4 +-- types/index.d.ts | 81 ++++++++++++++++++++++++++++++++++++++++++++---- 6 files changed, 100 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d3ea87e..64db7e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # Changelog +## v26.0.4 - 2026-08-30 + +- fix type declarations: `BpmnSequenceFlowEvent` is `flow.take` only since `bpmn-elements@18` no longer discards sequence flows, `flow.discard` and `flow.looped` are never emitted +- remove `wait` from `BpmnEngineEvent` — the engine itself never emits it; `waitFor` accepts any event name for custom emits +- add missing `activity.timer`, `activity.timeout`, `activity.signal`, `activity.catch`, `activity.discard`, and `activity.cancel` to `BpmnActivityEvent` +- add `BpmnProcessEvent`, `BpmnDefinitionEvent`, and `BpmnListenerEvent` types and type `IListenerEmitter.emit` with `(eventName, elementApi, execution)` +- document the above events in the API reference +- update dev dependency `camunda-bpmn-moddle@8` +- update peer dependency `smqp@14` + ## v26.0.3 - 2026-08-22 - support both `bpmn-moddle` 9 and 10 as promised by package diff --git a/docs/API.md b/docs/API.md index 490537d..0dbf837 100644 --- a/docs/API.md +++ b/docs/API.md @@ -637,6 +637,12 @@ Each activity and flow emits events when changing state. - `activity.start`: An activity is started - `activity.wait`: The activity is postponed for some reason, e.g. a user task is waiting to be signaled or a message is expected - `wait`: Same as above +- `activity.timer`: A timer was started +- `activity.timeout`: A timer timed out +- `activity.signal`: The activity was signaled +- `activity.catch`: The activity caught a message, signal, error, etc +- `activity.discard`: The activity was discarded +- `activity.cancel`: The activity was cancelled - `activity.end`: An activity has ended successfully - `activity.leave`: The execution left the activity - `activity.stop`: Activity run was stopped @@ -659,8 +665,8 @@ Events are emitted with api with execution properties ## Sequence flow events - `flow.take`: The sequence flow was taken -- `flow.discard`: The sequence flow was discarded -- `flow.looped`: The sequence is looped + +Since [bpmn-elements@18](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) non-selected sequence flows are no longer discarded, hence `flow.discard` and `flow.looped` are no longer emitted. # Expressions diff --git a/lib/index.cjs b/lib/index.cjs index 3426256..a5f9090 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -412,7 +412,7 @@ Engine.prototype.getState = async function getState() { /** * @template R - * @param {import('bpmn-engine').BpmnEngineEvent} eventName + * @param {import('bpmn-engine').BpmnEngineEvent | string} eventName * @returns {Promise} */ Engine.prototype.waitFor = function waitFor(eventName) { @@ -820,7 +820,7 @@ Execution.prototype.cancelActivity = function cancelActivity(payload) { /** * @template T - * @param {import('bpmn-engine').BpmnEngineEvent} eventName + * @param {import('bpmn-engine').BpmnEngineEvent | string} eventName * @returns {Promise} */ Execution.prototype.waitFor = function waitFor(...args) { diff --git a/package.json b/package.json index 928200e..9d55c65 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bpmn-engine", "description": "BPMN 2.0 execution engine. Open source javascript workflow engine.", - "version": "26.0.3", + "version": "26.0.4", "type": "module", "module": "./src/index.js", "main": "./lib/index.cjs", @@ -75,7 +75,7 @@ "bpmn-moddle": "^9.0.4", "bpmn-moddle-10": "npm:bpmn-moddle@^10.0.0", "c8": "^12.0.0", - "camunda-bpmn-moddle": "^7.0.1", + "camunda-bpmn-moddle": "^8.0.1", "chai": "^6.2.0", "chronokinesis": "^8.0.0", "debug": "^4.4.3", @@ -95,6 +95,6 @@ "bpmn-moddle": ">=9", "debug": ">=4", "moddle-context-serializer": ">=6", - "smqp": "^13.0.1" + "smqp": ">=13" } } diff --git a/src/index.js b/src/index.js index 99f3d97..e442322 100644 --- a/src/index.js +++ b/src/index.js @@ -265,7 +265,7 @@ Engine.prototype.getState = async function getState() { /** * @template R - * @param {import('bpmn-engine').BpmnEngineEvent} eventName + * @param {import('bpmn-engine').BpmnEngineEvent | string} eventName * @returns {Promise} */ Engine.prototype.waitFor = function waitFor(eventName) { @@ -673,7 +673,7 @@ Execution.prototype.cancelActivity = function cancelActivity(payload) { /** * @template T - * @param {import('bpmn-engine').BpmnEngineEvent} eventName + * @param {import('bpmn-engine').BpmnEngineEvent | string} eventName * @returns {Promise} */ Execution.prototype.waitFor = function waitFor(...args) { diff --git a/types/index.d.ts b/types/index.d.ts index 6ca37e3..734a560 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -16,20 +16,82 @@ declare module 'bpmn-engine' { Script, } from 'bpmn-elements'; - export type BpmnEngineEvent = 'error' | 'stop' | 'end' | 'wait'; - + /** + * Engine events. + * - `error`: A non-recoverable error has occurred + * - `stop`: Execution was stopped + * - `end`: Execution completed + */ + export type BpmnEngineEvent = 'error' | 'stop' | 'end'; + + /** + * Activity events emitted to the listener. + * - `activity.enter`: An activity is entered + * - `activity.start`: An activity is started + * - `activity.wait`: The activity is postponed for some reason, e.g. a user task is waiting to be signaled or a message is expected + * - `wait`: Same as above + * - `activity.timer`: A timer was started + * - `activity.timeout`: A timer timed out + * - `activity.signal`: The activity was signaled + * - `activity.catch`: The activity caught a message, signal, error, etc + * - `activity.discard`: The activity was discarded + * - `activity.cancel`: The activity was cancelled + * - `activity.end`: An activity has ended successfully + * - `activity.leave`: The execution left the activity + * - `activity.stop`: Activity run was stopped + * - `activity.throw`: A recoverable error was thrown + * - `activity.error`: A non-recoverable error has occurred + */ export type BpmnActivityEvent = | 'activity.enter' | 'activity.start' | 'activity.wait' | 'wait' + | 'activity.timer' + | 'activity.timeout' + | 'activity.signal' + | 'activity.catch' + | 'activity.discard' + | 'activity.cancel' | 'activity.end' | 'activity.leave' | 'activity.stop' | 'activity.throw' | 'activity.error'; - export type BpmnSequenceFlowEvent = 'flow.take' | 'flow.discard' | 'flow.looped'; + /** + * Sequence flow events emitted to the listener. + * - `flow.take`: The sequence flow was taken + * + * Since bpmn-elements@18 non-selected sequence flows are no longer discarded, so + * `flow.discard` and `flow.looped` are never emitted during execution. + */ + export type BpmnSequenceFlowEvent = 'flow.take'; + + /** + * Process events emitted to the listener. + */ + export type BpmnProcessEvent = + 'process.enter' | 'process.end' | 'process.leave' | 'process.error' | 'process.terminate' | 'process.discarded'; + + /** + * Definition events emitted to the listener. + */ + export type BpmnDefinitionEvent = 'definition.leave' | 'definition.stop' | 'definition.error'; + + /** + * All events emitted to the listener. Every `definition.#`, `process.#`, `activity.#`, and `flow.#` + * routing key published by bpmn-elements is forwarded verbatim, the named unions are the documented ones. + */ + export type BpmnListenerEvent = + | BpmnDefinitionEvent + | BpmnProcessEvent + | BpmnActivityEvent + | BpmnSequenceFlowEvent + | `definition.${string}` + | `process.${string}` + | `activity.${string}` + | `flow.${string}`; export type BpmnEngineRunningStatus = 'idle' | 'running' | 'stopped' | 'error'; @@ -40,7 +102,14 @@ declare module 'bpmn-engine' { } export interface IListenerEmitter { - emit(eventName: string, ...args: any[]): void; + /** + * Receive engine execution events + * @param eventName routing key of the event + * @param elementApi api of the element that emitted the event + * @param execution the engine execution + */ + emit(eventName: BpmnListenerEvent, elementApi: IApi, execution: Execution): unknown; + emit(eventName: string, ...args: any[]): unknown; } export interface BpmnEngineExecuteOptions extends EnvironmentOptions { @@ -111,7 +180,7 @@ declare module 'bpmn-engine' { addSource(options?: { sourceContext: SerializableContext }): void; - waitFor(eventName: BpmnEngineEvent): Promise; + waitFor(eventName: BpmnEngineEvent | string): Promise; } export function Execution( @@ -141,7 +210,7 @@ declare module 'bpmn-engine' { signal(message?: BpmnMessage, options?: { ignoreSameDefinition?: boolean }): void; cancelActivity(message?: BpmnMessage): void; - waitFor(eventName: BpmnEngineEvent): Promise; + waitFor(eventName: BpmnEngineEvent | string): Promise; } export interface JavaScripts extends IScripts { From 506bb578f5a2538fc2e6c12f983f3ba30cddf785 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Fri, 11 Sep 2026 06:56:52 +0200 Subject: [PATCH 13/14] upgrade bpmn-elements that come with major smqp@15 --- CHANGELOG.md | 5 +++++ package.json | 8 ++++---- src/index.js | 2 +- types/index.d.ts | 6 ------ 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64db7e9..87c8809 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog +## v26.0.5 - 2026-09-11 + +- require [smqp@>=15](https://github.com/paed01/smqp/blob/default/CHANGELOG.md) that have straighten out routing quirks +- remove Execution function type indicating that no new is required, but it is + ## v26.0.4 - 2026-08-30 - fix type declarations: `BpmnSequenceFlowEvent` is `flow.take` only since `bpmn-elements@18` no longer discards sequence flows, `flow.discard` and `flow.looped` are never emitted diff --git a/package.json b/package.json index 9d55c65..62064d4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bpmn-engine", "description": "BPMN 2.0 execution engine. Open source javascript workflow engine.", - "version": "26.0.4", + "version": "26.0.5", "type": "module", "module": "./src/index.js", "main": "./lib/index.cjs", @@ -71,7 +71,7 @@ "@types/mocha": "^10.0.10", "@types/node": "^20.19.40", "bent": "^7.3.12", - "bpmn-elements": "^18.0.15", + "bpmn-elements": "^18.0.24", "bpmn-moddle": "^9.0.4", "bpmn-moddle-10": "npm:bpmn-moddle@^10.0.0", "c8": "^12.0.0", @@ -91,10 +91,10 @@ "typescript": "^7.0.2" }, "peerDependencies": { - "bpmn-elements": ">=18", + "bpmn-elements": ">=18.0.25", "bpmn-moddle": ">=9", "debug": ">=4", "moddle-context-serializer": ">=6", - "smqp": ">=13" + "smqp": ">=15" } } diff --git a/src/index.js b/src/index.js index e442322..c1a2a76 100644 --- a/src/index.js +++ b/src/index.js @@ -334,7 +334,7 @@ Engine.prototype._getModdleContext = function getModdleContext(source) { /** * @param {Engine} engine - * @param {any[]} definitions + * @param {import('bpmn-elements').Definition[]} definitions * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [options] * @param {boolean} [isRecovered] */ diff --git a/types/index.d.ts b/types/index.d.ts index 734a560..2970491 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -183,12 +183,6 @@ declare module 'bpmn-engine' { waitFor(eventName: BpmnEngineEvent | string): Promise; } - export function Execution( - engine: Engine, - definitions: Definition[], - options?: BpmnEngineExecuteOptions, - isRecovered?: boolean - ): Execution; export class Execution { constructor(engine: Engine, definitions: Definition[], options?: BpmnEngineExecuteOptions, isRecovered?: boolean); options: BpmnEngineExecuteOptions; From c6f57e52948571255c1f8a8f7173172521e435cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?P=C3=A5l=20Edman?= Date: Mon, 14 Sep 2026 06:25:49 +0200 Subject: [PATCH 14/14] use @0dep/toc to generate the same --- .github/workflows/build-windows.yaml | 20 +++++ AGENTS.md | 9 +-- CHANGELOG.md | 49 +++++-------- README.md | 29 +++++--- appveyor.yml | 17 ----- docs/API.md | 2 +- docs/Examples.md | 2 +- docs/Upgrade.md | 44 +++++++++++ lib/index.cjs | 2 +- package.json | 6 +- scripts/generate-api-toc.js | 106 --------------------------- 11 files changed, 110 insertions(+), 176 deletions(-) create mode 100644 .github/workflows/build-windows.yaml delete mode 100644 appveyor.yml delete mode 100644 scripts/generate-api-toc.js diff --git a/.github/workflows/build-windows.yaml b/.github/workflows/build-windows.yaml new file mode 100644 index 0000000..b5eebb1 --- /dev/null +++ b/.github/workflows/build-windows.yaml @@ -0,0 +1,20 @@ +name: Build (Windows) + +on: + pull_request: + push: + branches: + - master + +jobs: + build: + runs-on: windows-latest + steps: + - name: Checkout + uses: actions/checkout@v7 + - name: Use Node.js from .nvmrc + uses: actions/setup-node@v7 + with: + node-version-file: .nvmrc + - run: npm i + - run: npx mocha diff --git a/AGENTS.md b/AGENTS.md index c327095..6f2838e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,14 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Commands -`.nvmrc` pins Node 18; `engines.node` is `>=18`. The test reporter `@bonniernews/hot-bev` and `mocha-cakes-2` UI handle parallel execution and Gherkin-style Features. +`.nvmrc` pins Node 20 (the `toc` bin needs >=20); `engines.node` is `>=18`. The test reporter `@bonniernews/hot-bev` and `mocha-cakes-2` UI handle parallel execution and Gherkin-style Features. -- `npm test` — runs `mocha -p` with the hot-bev reporter (parallel). `posttest` then runs `lint`, `toc` (regenerates API doc TOCs), and `dist`. -- `npm run wintest` — same suite, plain `mocha` (no `-p`/parallel; for environments where worker pools misbehave). +- `npm test` — runs `mocha -p` with the hot-bev reporter (parallel); the Windows CI build runs plain `npx mocha` (no `-p`, worker pools misbehave there). `posttest` then runs `lint`, `toc` (regenerates doc TOCs), `dist`, and `test:md`. - `npm run lint` — eslint (cached) + prettier check. - `npm run cov:html` / `npm run test:lcov` — coverage variants via c8. -- `npm run toc` — regenerates the table of contents in `docs/API.md` and `docs/Examples.md` via the in-repo `scripts/generate-api-toc.js` (no `markdown-toc` dep). It takes the markdown files as a comma- or space-separated argument, rewrites the block between `` and ``, skips the first H1 (the doc title — `docs/API.md` keeps a plain `# API Reference` title for this), and disambiguates duplicate anchors GitHub-style (`#getstate`, `#getstate-1`). -- `npm run test-md` — runs `texample` against `docs/API.md`, `docs/Examples.md`, `docs/Upgrade.md`. **Not** in the regular `posttest` chain; run manually when API doc examples change. +- `npm run toc` — regenerates the table of contents in `README.md` and `docs/*.md` with [`@0dep/toc`](https://0dep.se/toc/). It rewrites the block between `` and `` and lists every heading below the start marker, so the markers sit under the doc title to keep it out of the toc. Duplicate anchors are disambiguated GitHub-style (`#getstate`, `#getstate-1`). Output is prettier-compatible. +- `npm run test:md` — runs `texample` against `docs/API.md`, `docs/Examples.md`, `docs/Upgrade.md`. Runs as the last step of `posttest`, and standalone when doc examples change. The examples that mock HTTP with `nock` currently fail with `StatusError: Not Found` on this branch, independent of doc edits. - `npm run dist` — rollup builds `src/index.js` → `lib/index.cjs`. The footer trick `module.exports = Object.assign(exports.default, exports)` is what lets the CJS bundle's `require('bpmn-engine')` return the `Engine` constructor _and_ expose named exports — don't remove it. - Single test file: `npx mocha test/Engine-test.js`. Single test: append `--grep ""`. Default mocha timeout is 1000ms (see `.mocharc.json`); feature tests under `test/feature/` use the `mocha-cakes-2` BDD UI (`Feature`/`Scenario`/`Given`/`When`/`Then`). diff --git a/CHANGELOG.md b/CHANGELOG.md index 87c8809..a3a990b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,45 +1,32 @@ # Changelog -## v26.0.5 - 2026-09-11 +## v26.0.6 - 2026-09-14 -- require [smqp@>=15](https://github.com/paed01/smqp/blob/default/CHANGELOG.md) that have straighten out routing quirks -- remove Execution function type indicating that no new is required, but it is +Runs on [`bpmn-elements@18`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) that no longer discards sequence flows. Most diagrams run unchanged; see the [upgrade guide](/docs/Upgrade.md) for migration details. -## v26.0.4 - 2026-08-30 +### Breaking + +- major update [`bpmn-elements@18`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) — non-selected gateway flows are no longer discarded, so `flow.discard` and `flow.looped` are never emitted +- update to [`moddle-context-serializer@6`](https://github.com/paed01/moddle-context-serializer/blob/master/CHANGELOG.md) +- declare `bpmn-elements`, `bpmn-moddle`, `moddle-context-serializer`, `debug`, and `smqp` as peer dependencies (install and pin them yourself), see [peer dependencies](/README.md#peer-dependencies) +- require [`smqp@>=15`](https://github.com/paed01/smqp/blob/default/CHANGELOG.md) that has straightened out routing quirks +- support both `bpmn-moddle` 9 and 10 — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) -- fix type declarations: `BpmnSequenceFlowEvent` is `flow.take` only since `bpmn-elements@18` no longer discards sequence flows, `flow.discard` and `flow.looped` are never emitted +### Types + +- ship hand-maintained TypeScript declarations and type-check the test suite with them +- `typeResolver` option is a type resolver extender function, `JavaScripts` is callable without `new`, and `getScript` may return undefined +- `Execution` requires `new` +- `BpmnSequenceFlowEvent` is `flow.take` only - remove `wait` from `BpmnEngineEvent` — the engine itself never emits it; `waitFor` accepts any event name for custom emits - add missing `activity.timer`, `activity.timeout`, `activity.signal`, `activity.catch`, `activity.discard`, and `activity.cancel` to `BpmnActivityEvent` - add `BpmnProcessEvent`, `BpmnDefinitionEvent`, and `BpmnListenerEvent` types and type `IListenerEmitter.emit` with `(eventName, elementApi, execution)` -- document the above events in the API reference -- update dev dependency `camunda-bpmn-moddle@8` -- update peer dependency `smqp@14` - -## v26.0.3 - 2026-08-22 - -- support both `bpmn-moddle` 9 and 10 as promised by package - -## v26.0.2 - 2026-08-15 - -- fix type declarations: `typeResolver` option is a type resolver extender function, `JavaScripts` is callable without `new`, and `getScript` may return undefined -- type-check the test suite with the corrected `bpmn-elements@18` type declarations - -## v26.0.1 - 2026-07-07 - -- declare `bpmn-elements`, `bpmn-moddle`, `moddle-context-serializer`, and `debug` as peer dependencies (install and pin them yourself) -- verify `bpmn-moddle` 9 and 10 both work — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) - -## v26.0.0 - 2026-06-13 - -### Breaking - -- major update [`bpmn-elements@18`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md) — non-selected gateway flows are no longer discarded -- update to [`moddle-context-serializer@6`](https://github.com/paed01/moddle-context-serializer/blob/master/CHANGELOG.md) -- require `smqp@>=13` peer dependency ### Other -- ship hand-maintained TypeScript declarations +- document the above events in the API reference +- generate table of contents with [`@0dep/toc`](https://0dep.se/toc/) +- run the Windows build as a GitHub action instead of AppVeyor, Node version pinned by `.nvmrc` ## v25.0.1 - 2025-11-14 diff --git a/README.md b/README.md index 67ce2e6..c7e6739 100644 --- a/README.md +++ b/README.md @@ -2,24 +2,31 @@ [![Project Status: Active - The project has reached a stable, usable state and is being actively developed.](https://www.repostatus.org/badges/latest/active.svg)](https://www.repostatus.org/#active) -[![Build](https://github.com/paed01/bpmn-engine/actions/workflows/build.yaml/badge.svg)](https://github.com/paed01/bpmn-engine/actions/workflows/build.yaml)[![Build status](https://ci.appveyor.com/api/projects/status/670n39fivq1g3nu5/branch/master?svg=true)](https://ci.appveyor.com/project/paed01/bpmn-engine/branch/master)[![Coverage Status](https://coveralls.io/repos/github/paed01/bpmn-engine/badge.svg?branch=master)](https://coveralls.io/github/paed01/bpmn-engine?branch=master) +[![Build](https://github.com/paed01/bpmn-engine/actions/workflows/build.yaml/badge.svg)](https://github.com/paed01/bpmn-engine/actions/workflows/build.yaml)[![Build (Windows)](https://github.com/paed01/bpmn-engine/actions/workflows/build-windows.yaml/badge.svg)](https://github.com/paed01/bpmn-engine/actions/workflows/build-windows.yaml)[![Coverage Status](https://coveralls.io/repos/github/paed01/bpmn-engine/badge.svg?branch=master)](https://coveralls.io/github/paed01/bpmn-engine?branch=master) # Introduction BPMN 2.0 execution engine. Open source javascript workflow engine. +Documentation: + - [API](/docs/API.md) -- [Changelog](/CHANGELOG.md) - [Examples](/docs/Examples.md) - [Upgrade version](/docs/Upgrade.md) -- [Peer dependencies](#peer-dependencies) - [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) +- [Changelog](/CHANGELOG.md) + + + +- [Peer dependencies](#peer-dependencies) - [Supported elements](#supported-elements) - [Extensions](#extensions) - [Debug](#debug) -- [Example process](#a-pretty-image-of-a-process) +- [A pretty image of a process](#a-pretty-image-of-a-process) - [Acknowledgments](#acknowledgments) + + # Peer dependencies The runtime dependencies are declared as **peer dependencies**, so you install and pin them yourself and the engine reuses your single copy: @@ -28,13 +35,13 @@ The runtime dependencies are declared as **peer dependencies**, so you install a npm install bpmn-engine bpmn-elements bpmn-moddle moddle-context-serializer debug smqp ``` -| Peer dependency | Range | Role | -| ---------------------------------------------------------------------------------- | --------- | ------------------------------------------------- | -| [`bpmn-elements`](https://github.com/paed01/bpmn-elements) | `>=18` | element behaviour functions | -| [`bpmn-moddle`](https://github.com/bpmn-io/bpmn-moddle) | `>=9` | BPMN XML parser ([9 vs 10](/docs/bpmn-moddle.md)) | -| [`moddle-context-serializer`](https://github.com/paed01/moddle-context-serializer) | `>=6` | persistable source context | -| [`smqp`](https://github.com/paed01/smqp) | `^13.0.1` | message broker driving execution | -| [`debug`](https://github.com/debug-js/debug) | `>=4` | logging | +| Peer dependency | Range | Role | +| ---------------------------------------------------------------------------------- | ----------- | ------------------------------------------------- | +| [`bpmn-elements`](https://github.com/paed01/bpmn-elements) | `>=18.0.25` | element behaviour functions | +| [`bpmn-moddle`](https://github.com/bpmn-io/bpmn-moddle) | `>=9` | BPMN XML parser ([9 vs 10](/docs/bpmn-moddle.md)) | +| [`moddle-context-serializer`](https://github.com/paed01/moddle-context-serializer) | `>=6` | persistable source context | +| [`smqp`](https://github.com/paed01/smqp) | `>=15` | message broker driving execution | +| [`debug`](https://github.com/debug-js/debug) | `>=4` | logging | `bpmn-moddle` spans a major version on purpose — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) for the import change and how persisted state stays compatible across the upgrade. diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 47b2a20..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,17 +0,0 @@ -environment: - matrix: - - nodejs_version: '22' - -platform: - - x64 - -install: - - ps: Install-Product node $env:nodejs_version $env:platform - - npm install - -build: off - -test_script: - - node --version - - npm --version - - npm run wintest diff --git a/docs/API.md b/docs/API.md index 0dbf837..94919ed 100644 --- a/docs/API.md +++ b/docs/API.md @@ -26,7 +26,7 @@ - [Sequence flow events](#sequence-flow-events) - [Expressions](#expressions) - + # Engine diff --git a/docs/Examples.md b/docs/Examples.md index 2bbbbce..9605c7f 100644 --- a/docs/Examples.md +++ b/docs/Examples.md @@ -16,7 +16,7 @@ - [Traverse activities using definition shake](#traverse-activities-using-definition-shake) - [Persist state on events](#persist-state-on-events) - + # Execute diff --git a/docs/Upgrade.md b/docs/Upgrade.md index 67c6012..eb3afc5 100644 --- a/docs/Upgrade.md +++ b/docs/Upgrade.md @@ -1,5 +1,49 @@ # Upgrade + + +- [v25 → v26](#v25--v26) + - [Peer dependencies](#peer-dependencies) + - [Sequence flows are no longer discarded](#sequence-flows-are-no-longer-discarded) + - [Resuming state saved by v25](#resuming-state-saved-by-v25) + - [Types](#types) +- [< v14](#-v14) + + + +# v25 → v26 + +Version 26 runs on [`bpmn-elements@18`](https://github.com/paed01/bpmn-elements/blob/master/CHANGELOG.md), which refactors parallel gateway convergence and removes sequence flow discards altogether. Most diagrams run unchanged, but the behavioural changes are in the elements, not the engine, so read the [bpmn-elements upgrade guide](https://github.com/paed01/bpmn-elements/blob/master/docs/Upgrade.md) first. What follows is what changes from the engine's point of view. + +## Peer dependencies + +`bpmn-elements`, `bpmn-moddle`, `moddle-context-serializer`, `smqp`, and `debug` are peer dependencies. Install and pin them yourself, see [peer dependencies](/README.md#peer-dependencies) for the supported ranges. `smqp@15` or later is required. + +`bpmn-moddle` 9 and 10 are both supported — see [bpmn-moddle 9 vs 10](/docs/bpmn-moddle.md) for the import change. + +## Sequence flows are no longer discarded + +An untaken branch is left untouched instead of propagating a chain of discards. For an engine host this means: + +- the `flow.discard` and `flow.looped` events are never emitted, so the `listener` and `engine.broker` only see `flow.take`. The `BpmnSequenceFlowEvent` type reflects this. +- `discarded` counters on sequence flows and downstream activities stay at `0` for untaken branches. Track `flow.take` and `activity.end` instead. An activity discarded through the api still increments its own counter. +- multiple start events in a process are mutually exclusive entry points, an `IntermediateCatchEvent` without inbound flows is no longer started by default, and starting activities that are not start events are no longer auto-discarded. Diagrams relying on any of these must be redesigned. + +The details, including converging parallel gateways and shake output, are in the [bpmn-elements upgrade guide](https://github.com/paed01/bpmn-elements/blob/master/docs/Upgrade.md#no-more-flow-discards). + +## Resuming state saved by v25 + +State saved by v25 can be recovered and resumed by v26. `bpmn-elements@18` stamps definition state with a `stateVersion` and migrates older state on resume: start events are reconciled to the mutually exclusive rule and stale discard tokens left on process queues are acked, so they no longer strand process completion. No action is required beyond resuming; saving again stamps the current version. See [resuming state saved by v17](https://github.com/paed01/bpmn-elements/blob/master/docs/Upgrade.md#resuming-state-saved-by-v17). + +## Types + +The engine ships hand-maintained TypeScript declarations. Recompile a TypeScript host against them: + +- `Execution` requires `new` +- `BpmnSequenceFlowEvent` is `flow.take` only, and `wait` is removed from `BpmnEngineEvent` since the engine never emits it +- `BpmnActivityEvent` covers `activity.timer`, `activity.timeout`, `activity.signal`, `activity.catch`, `activity.discard`, and `activity.cancel`, and `BpmnProcessEvent`, `BpmnDefinitionEvent`, and `BpmnListenerEvent` are new +- the `typeResolver` option is a type resolver extender function, `JavaScripts` is callable without `new`, and `getScript` may return undefined + # < v14 Since v14 of the engine output is no longer shared between definition and processes. To upgrade a saved state before version 14 you can run the following script that adds process environment to state. diff --git a/lib/index.cjs b/lib/index.cjs index a5f9090..8e04630 100644 --- a/lib/index.cjs +++ b/lib/index.cjs @@ -481,7 +481,7 @@ Engine.prototype._getModdleContext = function getModdleContext(source) { /** * @param {Engine} engine - * @param {any[]} definitions + * @param {import('bpmn-elements').Definition[]} definitions * @param {import('bpmn-engine').BpmnEngineExecuteOptions} [options] * @param {boolean} [isRecovered] */ diff --git a/package.json b/package.json index 62064d4..d6fa3cf 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "bpmn-engine", "description": "BPMN 2.0 execution engine. Open source javascript workflow engine.", - "version": "26.0.5", + "version": "26.0.6", "type": "module", "module": "./src/index.js", "main": "./lib/index.cjs", @@ -34,10 +34,9 @@ "test": "mocha -p -R @bonniernews/hot-bev", "posttest": "npm run lint && npm run toc && npm run dist && npm run test:md", "lint": "eslint . --cache && prettier . -c --cache", - "wintest": "mocha", "cov:html": "c8 -r html -r text mocha -p -R @bonniernews/hot-bev", "test:lcov": "c8 -r lcov mocha && npm run lint", - "toc": "node scripts/generate-api-toc ./docs/API.md,./docs/Examples.md", + "toc": "toc README.md docs/API.md docs/Examples.md docs/Upgrade.md", "test:md": "texample ./docs/API.md,./docs/Examples.md,./docs/Upgrade.md -c .mocharc.json", "dist": "rollup -c", "prepack": "npm run dist" @@ -63,6 +62,7 @@ } }, "devDependencies": { + "@0dep/toc": "^1.0.1", "@bonniernews/hot-bev": "^0.4.0", "@eslint/js": "^10.0.1", "@rollup/plugin-commonjs": "^29.0.0", diff --git a/scripts/generate-api-toc.js b/scripts/generate-api-toc.js deleted file mode 100644 index daf4666..0000000 --- a/scripts/generate-api-toc.js +++ /dev/null @@ -1,106 +0,0 @@ -#!/usr/bin/env node -/* eslint-disable no-console */ -import { readFile, writeFile } from 'node:fs/promises'; -import { fileURLToPath } from 'node:url'; -import { dirname, resolve } from 'node:path'; - -const TOC_START = ''; -const TOC_END = ''; - -const here = dirname(fileURLToPath(import.meta.url)); -const repoRoot = resolve(here, '..'); - -const args = process.argv.slice(2); -const files = (args.length ? args : ['README.md']).flatMap((arg) => arg.split(',')).filter(Boolean); - -let exitCode = 0; -for (const arg of files) { - try { - await processFile(resolve(repoRoot, arg)); - } catch (err) { - console.error(`${arg}: ${err.message}`); - exitCode = 1; - } -} -process.exit(exitCode); - -/** @param {string} mdPath */ -async function processFile(mdPath) { - const source = await readFile(mdPath, 'utf8'); - - const inFence = (() => { - const fences = []; - let open = false; - for (const [i, line] of source.split('\n').entries()) { - if (/^```/.test(line)) { - open = !open; - } - fences[i] = open; - } - return (i) => fences[i]; - })(); - - const lines = source.split('\n'); - /** @type {Array<{ level: number, text: string, slug: string }>} */ - const headlines = []; - const slugCounts = new Map(); - let firstHeadlineIdx = -1; - let titleSkipped = false; - - for (const [i, line] of lines.entries()) { - if (inFence(i)) continue; - const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line); - if (!match) continue; - const level = match[1].length; - if (level === 1 && !titleSkipped) { - titleSkipped = true; - continue; - } - const text = match[2].replace(/\*/g, '\\*'); - const baseSlug = slugify(match[2]); - const n = slugCounts.get(baseSlug) ?? 0; - slugCounts.set(baseSlug, n + 1); - const slug = n === 0 ? baseSlug : `${baseSlug}-${n}`; - headlines.push({ level, text, slug }); - if (firstHeadlineIdx === -1) firstHeadlineIdx = i; - } - - if (firstHeadlineIdx === -1 || headlines.length === 0) { - console.error(`${mdPath}: no non-title headlines found; nothing to do.`); - return; - } - - const minLevel = Math.min(...headlines.map((h) => h.level)); - const tocLines = headlines.map(({ level, text, slug }) => `${' '.repeat(level - minLevel)}- [${text}](#${slug})`); - const tocBlock = [TOC_START, '', ...tocLines, '', TOC_END].join('\n'); - - const startIdx = lines.findIndex((l) => l.trim() === TOC_START); - const endIdx = lines.findIndex((l) => l.trim() === TOC_END); - - let updated; - if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) { - updated = [...lines.slice(0, startIdx), tocBlock, ...lines.slice(endIdx + 1)].join('\n'); - } else { - updated = [...lines.slice(0, firstHeadlineIdx), tocBlock, '', ...lines.slice(firstHeadlineIdx)].join('\n'); - } - - if (updated === source) { - console.log(`${mdPath}: TOC already up to date.`); - } else { - await writeFile(mdPath, updated); - console.log(`${mdPath}: wrote TOC with ${headlines.length} entries.`); - } -} - -/** - * @param {string} text - * @returns {string} - */ -function slugify(text) { - return text - .toLowerCase() - .replace(/[`*_~]/g, '') - .replace(/[^\w\s-]/g, '') - .trim() - .replace(/\s+/g, '-'); -}