diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index f59caaf0..9390c036 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -76,7 +76,7 @@ jobs:
if: env.CI_SOURCE_TOKEN != ''
with:
repository: Miragon/corporate-identity
- ref: v1.10.0
+ ref: v1.26.0
token: ${{ env.CI_SOURCE_TOKEN }}
path: .ci-source
sparse-checkout: plugins/miragon-brand
diff --git a/README.md b/README.md
index 1979f5c3..32d55168 100644
--- a/README.md
+++ b/README.md
@@ -114,7 +114,7 @@ Details: [library use](https://miragon.github.io/bpmn-cli/library), [validators]
The docs site, [miragon.github.io/bpmn-cli](https://miragon.github.io/bpmn-cli/), is built from [docs/](docs/):
- [Command reference](https://miragon.github.io/bpmn-cli/commands): every command with its options
-- [Reference](https://miragon.github.io/bpmn-cli/reference): ids, kinds, triggers, placement, set keys, output, errors and exit codes
+- [Syntax and output](https://miragon.github.io/bpmn-cli/reference): ids, kinds, triggers, placement, set keys, output, errors and exit codes
- [Ops JSON](https://miragon.github.io/bpmn-cli/ops): the batch format of `bpmn apply`
- [Layout and formatting](https://miragon.github.io/bpmn-cli/layout): layout modes and the diagram-only commands
- [The contract and its limits](https://miragon.github.io/bpmn-cli/contract): what the CLI guarantees, what a write changes, limitations
diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts
index a852601e..b7e359ff 100644
--- a/docs/.vitepress/config.ts
+++ b/docs/.vitepress/config.ts
@@ -23,9 +23,8 @@ export default miragonDocs({
link: "/getting-started",
activeMatch: "^/(getting-started|claude-code-plugin|walkthrough|contract)",
},
- { text: "Commands", link: "/commands", activeMatch: "^/(commands|ops|camunda|layout|bpmnlint)" },
+ { text: "Commands", link: "/commands", activeMatch: "^/(commands|reference|ops|camunda|layout|bpmnlint)" },
{ text: "Library", link: "/library", activeMatch: "^/(library|validators)" },
- { text: "Reference", link: "/reference", activeMatch: "^/reference" },
],
// One sidebar for every page: the CLI is small enough to show it all.
@@ -43,6 +42,7 @@ export default miragonDocs({
text: "Using the CLI",
items: [
{ text: "Commands", link: "/commands" },
+ { text: "Syntax and output", link: "/reference" },
{ text: "Ops JSON (bpmn apply)", link: "/ops" },
{ text: "Camunda 7 and Camunda 8", link: "/camunda" },
{ text: "Layout and formatting", link: "/layout" },
@@ -57,9 +57,9 @@ export default miragonDocs({
],
},
{
- text: "Reference",
+ text: "Contributing",
items: [
- { text: "Ids, kinds and keys", link: "/reference" },
+ { text: "Architecture", link: "/architecture" },
],
},
],
diff --git a/docs/.vitepress/miragon/manifest.json b/docs/.vitepress/miragon/manifest.json
index efd84433..e269c786 100644
--- a/docs/.vitepress/miragon/manifest.json
+++ b/docs/.vitepress/miragon/manifest.json
@@ -1,6 +1,6 @@
{
"$kommentar": "Generiert von scripts/sync-assets.mjs in Miragon/corporate-identity, nicht von Hand ändern. Jede Datei, die ein Repo aus der Skill docs-site vendort: Pfad im Repo, Quelle unter plugins/miragon-brand/skills/docs-site/assets/ und sha256. check.mjs prüft die Kopie damit.",
- "version": "1.25.1",
+ "version": "1.26.0",
"dateien": [
{
"pfad": "docs/.vitepress/miragon/check.mjs",
@@ -42,6 +42,11 @@
"quelle": "miragon/theme/StarPrompt.vue",
"sha256": "38a7de32637193b43f9eb3dedd79584a9f11ee1a0ffd4f428aa2f30ba3af18fa"
},
+ {
+ "pfad": "docs/.vitepress/miragon/theme/ZoomOverlay.vue",
+ "quelle": "miragon/theme/ZoomOverlay.vue",
+ "sha256": "387a2ecd0465e4b158431f34edb7dea2feff5a62cf84b5622520c43829c290c7"
+ },
{
"pfad": "docs/.vitepress/miragon/theme/cd-tokens.generated.css",
"quelle": "miragon/theme/cd-tokens.generated.css",
@@ -50,12 +55,12 @@
{
"pfad": "docs/.vitepress/miragon/theme/index.ts",
"quelle": "miragon/theme/index.ts",
- "sha256": "dda90b8c056e558ac01f2de1a4f38c918d50339af5d59c87ddd710e09195ccd9"
+ "sha256": "450a3ab3603426b0b7aa1d751976998b5e52341ed110245ada6658f9d1097b99"
},
{
"pfad": "docs/.vitepress/miragon/theme/miragon.css",
"quelle": "miragon/theme/miragon.css",
- "sha256": "5c43dc675cf7d718e42f7089e1bb274f1198bf1049d7997b108fe3724f05a4df"
+ "sha256": "3253247af1fb53006694a2db7a3a1d001cb6875dfb64fbb55a840c062df74344"
},
{
"pfad": "docs/.vitepress/miragon/theme/useStars.ts",
diff --git a/docs/.vitepress/miragon/theme/ZoomOverlay.vue b/docs/.vitepress/miragon/theme/ZoomOverlay.vue
new file mode 100644
index 00000000..6401a006
--- /dev/null
+++ b/docs/.vitepress/miragon/theme/ZoomOverlay.vue
@@ -0,0 +1,81 @@
+
+
+
+
+
+
+
+
diff --git a/docs/.vitepress/miragon/theme/index.ts b/docs/.vitepress/miragon/theme/index.ts
index 1887fe09..204bddfc 100644
--- a/docs/.vitepress/miragon/theme/index.ts
+++ b/docs/.vitepress/miragon/theme/index.ts
@@ -29,6 +29,7 @@ import "./miragon.css"
import LegalRow from "./LegalRow.vue"
import GitHubStars from "./GitHubStars.vue"
import StarPrompt from "./StarPrompt.vue"
+import ZoomOverlay from "./ZoomOverlay.vue"
// Im Titel jeder Seite steht der Komet statt der Wortmarke, daneben der Produktname
// (theme/miragon.css, Abschnitt TITEL: KOMET UND PRODUKTNAME).
@@ -40,7 +41,8 @@ const Komet: FunctionalComponent = () =>
h("path", { d: KOMET, fill: "#00E676" }),
])
// Das Layout von VitePress mit GitHub und Sternen im Header, der Bitte um einen Stern am
-// Seitenende und der Rechtsleiste ganz unten. Slots von außen reicht es weiter,
+// Seitenende, der Rechtsleiste ganz unten und dem Vollbild für Bilder und Diagramme
+// (ZoomOverlay.vue). Slots von außen reicht es weiter,
// auch einen eigenen layout-bottom, der dann über der Rechtsleiste steht.
const Layout: FunctionalComponent = (_, { slots }) =>
h(DefaultTheme.Layout, null, {
@@ -48,7 +50,7 @@ const Layout: FunctionalComponent = (_, { slots }) =>
"nav-bar-title-before": () => [slots["nav-bar-title-before"]?.(), h(Komet)],
"nav-bar-content-after": () => [slots["nav-bar-content-after"]?.(), h(GitHubStars)],
"doc-footer-before": () => [slots["doc-footer-before"]?.(), h(StarPrompt)],
- "layout-bottom": () => [slots["layout-bottom"]?.(), h(LegalRow)],
+ "layout-bottom": () => [slots["layout-bottom"]?.(), h(LegalRow), h(ZoomOverlay)],
})
const MiragonTheme: Theme = {
diff --git a/docs/.vitepress/miragon/theme/miragon.css b/docs/.vitepress/miragon/theme/miragon.css
index 37a3aa9c..958ba867 100644
--- a/docs/.vitepress/miragon/theme/miragon.css
+++ b/docs/.vitepress/miragon/theme/miragon.css
@@ -643,6 +643,76 @@
color: var(--cd-gruen);
}
+/* =========================================================
+ ZOOM
+ Bilder, Mermaid-Diagramme und Elemente mit data-zoom öffnen sich im
+ Vollbild (ZoomOverlay.vue). Das Overlay ist die Seite selbst, ohne
+ Deckkraft, damit die Farben des Diagramms hell und dunkel bleiben. Die
+ Kopie wächst bis an den Rand, das Seitenverhältnis bleibt.
+ ========================================================= */
+.vp-doc :is(img, .mermaid, [data-zoom]):not([data-zoom="off"], [data-zoom="off"] *, a *) {
+ cursor: zoom-in;
+}
+.miragon-zoom-offen {
+ overflow: hidden;
+}
+.miragon-zoom {
+ position: fixed;
+ inset: 0;
+ z-index: 200;
+ display: flex;
+ background: var(--cd-ui-seite);
+ cursor: zoom-out;
+}
+.miragon-zoom-buehne {
+ flex: 1;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-width: 0;
+ padding: 56px 24px 24px;
+ overflow: auto;
+}
+.miragon-zoom-kopie {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ cursor: default;
+}
+.miragon-zoom-kopie svg,
+img.miragon-zoom-kopie,
+.miragon-zoom-kopie img {
+ width: 100%;
+ height: 100%;
+ max-width: none !important;
+ object-fit: contain;
+}
+.miragon-zoom-zu {
+ position: absolute;
+ top: 12px;
+ right: 12px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ border: 1px solid var(--cd-ui-linie);
+ border-radius: 999px;
+ background: var(--cd-ui-seite);
+ color: var(--cd-ui-text);
+ cursor: pointer;
+ transition: border-color 0.25s;
+}
+.miragon-zoom-zu:hover {
+ border-color: var(--cd-ui-kontur);
+}
+.miragon-zoom-zu svg {
+ width: 14px;
+}
+
/* =========================================================
FOKUS
Sichtbarer Tastatur-Fokus über die Rolle: hell blau-link, dunkel blau-hell.
diff --git a/docs/architecture.md b/docs/architecture.md
new file mode 100644
index 00000000..7c79498d
--- /dev/null
+++ b/docs/architecture.md
@@ -0,0 +1,359 @@
+# Architecture
+
+How `src/` is built, and how a command gets from the terminal into the BPMN model.
+`test/architecture.test.ts` checks every rule on this page. The folder table and where new code
+goes are in [CONTRIBUTING.md](https://github.com/Miragon/bpmn-cli/blob/main/CONTRIBUTING.md#how-the-package-is-laid-out).
+
+## One domain, three ways in
+
+bpmn-cli runs as the `bpmn` command, as a Node library (`@miragon/bpmn-cli/node`) and as a
+browser library (`@miragon/bpmn-cli`). All three must change a model in exactly the same way.
+So what a change means lives once, in the **domain**, which knows nothing about files, flags
+or `process`. Around it, the **application** offers one use case per thing a caller can do.
+At the edge, **adapters** translate between the outside world and those use cases. This is a
+hexagonal architecture (ports and adapters).
+
+```mermaid
+flowchart TB
+ terminal(["Terminal"]) --> cli
+ lib(["Node / browser code"]) --> portIn
+
+ cli["CLI
adapters/in/cli"] --> portIn
+ portIn{{"Use case
application/port/in"}} --> service["Service
application/service"]
+ service --> domain["Domain
domain/"]
+ service --> portOut{{"Port
application/port/out"}}
+ portOut --> files["Files
adapters/out/files"]
+ portOut --> memory["Memory
adapters/out/memory"]
+
+ classDef adapter stroke:#2f80ed,stroke-width:2px
+ classDef app stroke:#f2994a,stroke-width:2px
+ classDef core stroke:#27ae60,stroke-width:3px
+ class cli,files,memory adapter
+ class portIn,service,portOut app
+ class domain core
+```
+
+The frame colour shows the layer: blue for adapters, orange for the application, green for the
+domain. Imports point inwards only: adapters → application → domain.
+The entries in the root of `src/` (`cli.ts`, `node.ts`, `api.ts`) are the only files that know
+every layer. They create the services with `new` and pass the adapters in. There is no
+container.
+
+| rule (checked by the test) | why |
+| --- | --- |
+| `domain/` imports no application, adapter or entry | the domain runs anywhere, also in a browser |
+| `application/` imports no adapter | a service does not know whether it writes to disk or to memory |
+| only adapters and entries import `node:*` | the browser bundle stays free of Node |
+| `adapters/in` knows use cases only by their interface | the entry decides which service is behind it |
+| each service implements one use case and calls no other service | one way in, no hidden orchestration |
+
+## The journey of `bpmn add`
+
+```sh
+bpmn add order.bpmn userTask "Check invoice" --after Event_OrderReceived
+```
+
+```mermaid
+flowchart TB
+ user(["bpmn add"]) --> s1["1 · AddElementCommand"] --> s2{{"2 · EditDocumentUseCase"}} --> s3["3 · EditDocumentService"] --> s4["4 · EditBatchService"] --> s5["5 · ModelCommandService"] --> s6["6 · ElementCreationService"]
+ s3 <-- "read · write" --> s7[("7 · FileDocumentStore")]
+ s3 -- result --> s8["8 · MutationText"]
+
+ classDef adapter stroke:#2f80ed,stroke-width:2px
+ classDef app stroke:#f2994a,stroke-width:2px
+ classDef core stroke:#27ae60,stroke-width:3px
+ class s1,s7,s8 adapter
+ class s2,s3 app
+ class s4,s5,s6 core
+```
+
+Eight stops, framed in the colours of their layer. The file system (7) is only touched by the
+service, through a port; the domain never sees it.
+
+The snippets below are the real functions, shortened: checks, options and edge cases are left
+out (`…`), the calls that hand over to the next stop are kept.
+
+### 1. Flags become data
+
+When the CLI starts, `cli.ts` creates the `Cli`, and every command class registers its verb with
+commander. For `add`, commander later calls `run()` with the parsed flags. It turns them into a
+plain object, the same shape `bpmn apply` reads from JSON, and hands it on.
+
+```ts
+// src/adapters/in/cli/AddElementCommand.ts
+export class AddElementCommand extends MutationCommand {
+ register(program: Command): void {
+ this.define(program, 'add')
+ .option('--after ')
+ .option('--before ')
+ // …
+ .action((file, kind, name, keyValues, options) =>
+ this.run(file, kind, name, keyValues, options),
+ )
+ }
+
+ private async run(
+ file: string,
+ kind: string,
+ name: string | undefined,
+ keyValues: string[],
+ options: RawOpts,
+ ) {
+ const opts = this.mutationOptions(options)
+ await this.cli.guarded(async () => {
+ this.checkPlacementFlags(options)
+ const op: AddElement = {
+ op: 'add',
+ kind, // 'userTask'
+ name, // 'Check invoice'
+ after: options['after'], // 'Event_OrderReceived'
+ // …
+ }
+ await this.mutate(file, [op], opts)
+ }, opts.json)
+ }
+}
+```
+
+`guarded()` catches any error and prints it with its code and hint. From here on nobody knows
+about flags any more.
+
+### 2. Through the port
+
+`mutate()` lives in the base class of every changing command. It calls the use case and prints
+what comes back. The CLI only knows the interface `EditDocumentUseCase`; which service is
+behind it, `cli.ts` decided at startup.
+
+```ts
+// src/adapters/in/cli/MutationCommand.ts
+protected async mutate(
+ file: string,
+ ops: EditCommand[],
+ opts: MutationFlags,
+) {
+ const result = await this.cli.useCases.editDocument.edit({
+ ...opts,
+ source: { file },
+ commands: ops,
+ })
+ // step 8
+ await this.printMutation(result, opts, this.cli.io.stdout)
+}
+```
+
+```ts
+// src/application/port/in/EditDocumentUseCase.ts
+export interface EditDocumentUseCase {
+ edit(command: EditDocumentUseCase.Command): Promise
+}
+
+export declare namespace EditDocumentUseCase {
+ interface Command extends FileMutationOptions {
+ source: DocumentSource // { file } or an already loaded { doc }
+ commands: EditCommand[]
+ }
+}
+```
+
+### 3. Load, run, save
+
+The service is short on purpose: get the document, let the domain work, write the result.
+`load`, `save` and `settings` are outbound ports, handed in by the constructor.
+
+```ts
+// src/application/service/EditDocumentService.ts
+export class EditDocumentService implements EditDocumentUseCase {
+ constructor(
+ private readonly load: LoadDocumentPort,
+ private readonly save: SaveDocumentPort,
+ private readonly settings: LoadProjectSettingsPort,
+ ) {}
+
+ async edit({ source, commands, ...opts }: EditDocumentUseCase.Command) {
+ // step 7: read the file
+ const doc = 'doc' in source
+ ? source.doc
+ : await Doc.fromXml(await this.load.read(source.file), source.file)
+ const target = opts.out ?? doc.file
+
+ // the project's .bpmnlintrc, if any
+ const lint = await this.settings.lint(target, opts)
+
+ // step 4: the domain does the work
+ const batchOpts = lint.applyTo(opts, target)
+ const batch = new EditBatchService(doc, commands, batchOpts)
+ const result = await batch.run(target)
+
+ // step 7: write the file
+ if (!opts.dryRun) {
+ result.written = await this.save.save(target, result.xml, { … })
+ }
+ return result
+ }
+}
+```
+
+In the CLI the ports are a `FileDocumentStore` and `BpmnlintProjectSettings`; in `applyToXml()`
+they are an `InMemoryDocumentStore` and `NoProjectSettings`. The service cannot tell.
+
+### 4. The batch
+
+The domain starts here. `EditBatchService` runs all commands of one call on the `Doc` in
+memory, then validates, lays out the drawing and writes only the changed parts back into the
+original text. If anything throws, nothing is written: the caller keeps its input.
+
+```ts
+// src/domain/batch/EditBatchService.ts
+async run(file = this.doc.file): Promise {
+ // lossless import, Camunda 7 / 8 rules loaded
+ await this.guard()
+
+ // remember the state before: warnings already there, the drawing
+ const findings = await FindingBaseline.capture(this.doc, …)
+ const diagram = await BatchDrawing.capture(this.doc, …)
+
+ // step 5: run every command on the model
+ const { batch } = await this.runCommands(diagram)
+
+ // new errors refuse the write
+ await this.validate(findings, batch)
+
+ // lay out the drawing, rewrite only what changed in the text
+ const drawn = await diagram.apply(this.edit, batch.ops, batch.changes)
+ const xml = await this.keptText(drawn.xml, …)
+
+ return this.result(file, batch, xml, drawn.layout, …)
+}
+```
+
+### 5. The right service for the op
+
+`ModelCommandService` runs the commands in order and hands each one to the service that knows
+it. Commands that only move shapes (`place`, `align`, `color`, …) are left for the layout in
+step 4.
+
+```ts
+// src/domain/batch/ModelCommandService.ts
+private dispatch(op: EditCommand): ChangeSet {
+ if (isDiagramCommand(op)) return new ChangeSet()
+ const { doc, edit } = this
+ switch (op.op) {
+ case 'add':
+ return new ElementCreationService(doc, edit).add(op)
+ case 'connect':
+ return new ConnectionService(doc, edit).connect(op)
+ case 'set':
+ return new PropertyService(doc, edit).set(op)
+ case 'remove':
+ return new ElementRemovalService(doc, edit).remove(op)
+ // … retype, move, order, ext, split
+ }
+}
+```
+
+### 6. The model changes
+
+`ElementCreationService` makes the new element in the bpmn-moddle tree of the `Doc`, places it
+after `Event_OrderReceived` (which creates the sequence flow), and records everything it did in
+a `ChangeSet`.
+
+```ts
+// src/domain/editing/ElementCreationService.ts
+add(op: AddElement): ChangeSet {
+ const cs = new ChangeSet()
+ // 'userTask' → bpmn:UserTask
+ const { def, trigger } = ElementCreationService.resolveKind(op.kind)
+ const el = this.addFlowNode(op, def, trigger, cs)
+ if (op.set) {
+ const values = { op: 'set', id: idOf(el), values: op.set }
+ cs.merge(new PropertyService(this.doc, this.edit).set(values))
+ }
+ return cs
+}
+
+private addFlowNode(
+ op: AddElement,
+ def: KindDef,
+ trigger: Trigger | undefined,
+ cs: ChangeSet,
+): El {
+ // 'Activity_CheckInvoice'
+ const id = this.edit.allocator.explicitOr(op.id, …)
+ const el = this.doc.create(def.type, { id, name: op.name })
+ cs.create({ id, kind: def.kind, name: op.name })
+
+ // --after: splice in behind the anchor and add the sequence flow
+ new NodePlacementService(this.doc, this.edit, op).place(el, cs)
+ return el
+}
+```
+
+### 7. The file
+
+`FileDocumentStore` implements both file ports. It is the only class on this path that imports
+`node:fs`.
+
+```ts
+// src/adapters/out/files/FileDocumentStore.ts
+export class FileDocumentStore
+ implements LoadDocumentPort, SaveDocumentPort
+{
+ // decoded in the encoding the XML declares
+ read(file: string): Promise {
+ return readXml(file)
+ }
+
+ async save(target: string, xml: string, opts: SaveOptions) {
+ const { source, unchanged, backup } = opts
+ // a no-op leaves the file alone
+ if (unchanged && target === source) return false
+ if (backup && source) await FileDocumentStore.backup(source)
+ // a failed write leaves the old file
+ await writeAtomic(target, xml)
+ return true
+ }
+}
+```
+
+### 8. The answer
+
+Back in `MutationCommand`, the result becomes a report (`MutationReportBuilder`) and then text
+(`MutationText`), both in `adapters/in/report/`. The library returns the same report, so the
+CLI and `applyToXml()` describe a change in the same words.
+
+```txt
+created userTask Activity_CheckInvoice "Check invoice" - after Event_OrderReceived
+created sequenceFlow Flow_OrderReceivedToCheckInvoice - Event_OrderReceived -> Activity_CheckInvoice
+layout: ok - full (engine-owned diagram: redrawn)
+written: order.bpmn
+```
+
+### Other ways in
+
+- `bpmn apply` differs only in step 1: it reads the commands as JSON instead of flags.
+- `mutateFile()` (`node.ts`) starts at step 2.
+- `applyToXml()` (`api.ts`) starts at step 2, with memory in step 7 and no project
+ `.bpmnlintrc`, and returns the XML instead of saving it.
+
+From step 2 onwards, all of them run the same code.
+
+## One definition per command
+
+Each command (`add`, `connect`, …) is plain data with one **definition** in
+`domain/commands/definitions/`. The definition names its fields, which of them hold ids, and
+its checks. Every place that needs to know a command reads the definition, so they cannot drift
+apart:
+
+```mermaid
+flowchart LR
+ def["AddElementDefinition"] --> parser["bpmn apply
accepts and checks the JSON"]
+ def --> schema["OPS_SCHEMA
for agents and editors"]
+ def --> help["bpmn add --help
option texts"]
+
+ classDef core stroke:#27ae60,stroke-width:2px
+ class def core
+```
+
+A new command therefore needs a definition, a `case` in `ModelCommandService` and a service in
+`domain/editing/`. `bpmn apply` and `applyToXml()` accept it from then on. A verb of its own on
+the command line (`bpmn `) is optional and lives in `adapters/in/cli/`.
diff --git a/docs/camunda.md b/docs/camunda.md
index 86ec5600..f3a32d36 100644
--- a/docs/camunda.md
+++ b/docs/camunda.md
@@ -47,11 +47,17 @@ approves the rest.
```sh
bpmn new claim.bpmn --name "Claim handling" --target camunda7
bpmn add claim.bpmn start "Claim received"
-bpmn add claim.bpmn serviceTask "Check coverage" --after Event_ClaimReceived camunda:type=external camunda:topic=check-coverage
-bpmn ext add claim.bpmn Activity_CheckCoverage camunda:inputParameter name=amount --body '${amount}'
-bpmn add claim.bpmn boundary:error "Not covered" --on Activity_CheckCoverage --error "Not covered" --error-code NOT_COVERED
+bpmn add claim.bpmn serviceTask "Check coverage" \
+ --after Event_ClaimReceived camunda:type=external \
+ camunda:topic=check-coverage
+bpmn ext add claim.bpmn Activity_CheckCoverage camunda:inputParameter \
+ name=amount --body '${amount}'
+bpmn add claim.bpmn boundary:error "Not covered" \
+ --on Activity_CheckCoverage --error "Not covered" \
+ --error-code NOT_COVERED
bpmn add claim.bpmn end "Claim rejected" --after Event_NotCovered
-bpmn add claim.bpmn userTask "Approve claim" --after Activity_CheckCoverage camunda:candidateGroups=claims
+bpmn add claim.bpmn userTask "Approve claim" \
+ --after Activity_CheckCoverage camunda:candidateGroups=claims
bpmn add claim.bpmn end "Claim settled" --after Activity_ApproveClaim
```
@@ -91,16 +97,27 @@ flow. The process then waits for the payment message.
```sh
bpmn new invoice.bpmn --name "Invoice approval" --target camunda8
bpmn add invoice.bpmn start "Invoice received"
-bpmn add invoice.bpmn serviceTask "Check invoice" --after Event_InvoiceReceived
-bpmn ext add invoice.bpmn Activity_CheckInvoice zeebe:taskDefinition type=check-invoice retries=3
-bpmn ext add invoice.bpmn Activity_CheckInvoice zeebe:input source==invoice.amount target=amount
-bpmn add invoice.bpmn exclusiveGateway "Amount over 1000?" --after Activity_CheckInvoice
-bpmn add invoice.bpmn userTask "Approve invoice" --after Gateway_AmountOver1000 --flow-name yes --condition '= amount > 1000'
-bpmn ext add invoice.bpmn Activity_ApproveInvoice zeebe:assignmentDefinition candidateGroups=finance
-bpmn add invoice.bpmn exclusiveGateway --id Gateway_Approved --after Activity_ApproveInvoice
-bpmn connect invoice.bpmn Gateway_AmountOver1000 Gateway_Approved --name no --default
-bpmn add invoice.bpmn receiveTask "Wait for payment" --after Gateway_Approved --message "Payment received"
-bpmn ext add invoice.bpmn Activity_WaitForPayment zeebe:subscription correlationKey==invoiceId
+bpmn add invoice.bpmn serviceTask "Check invoice" \
+ --after Event_InvoiceReceived
+bpmn ext add invoice.bpmn Activity_CheckInvoice zeebe:taskDefinition \
+ type=check-invoice retries=3
+bpmn ext add invoice.bpmn Activity_CheckInvoice zeebe:input \
+ source==invoice.amount target=amount
+bpmn add invoice.bpmn exclusiveGateway "Amount over 1000?" \
+ --after Activity_CheckInvoice
+bpmn add invoice.bpmn userTask "Approve invoice" \
+ --after Gateway_AmountOver1000 --flow-name yes \
+ --condition '= amount > 1000'
+bpmn ext add invoice.bpmn Activity_ApproveInvoice \
+ zeebe:assignmentDefinition candidateGroups=finance
+bpmn add invoice.bpmn exclusiveGateway --id Gateway_Approved \
+ --after Activity_ApproveInvoice
+bpmn connect invoice.bpmn Gateway_AmountOver1000 Gateway_Approved \
+ --name no --default
+bpmn add invoice.bpmn receiveTask "Wait for payment" \
+ --after Gateway_Approved --message "Payment received"
+bpmn ext add invoice.bpmn Activity_WaitForPayment zeebe:subscription \
+ correlationKey==invoiceId
bpmn add invoice.bpmn end "Invoice settled" --after Activity_WaitForPayment
```
diff --git a/docs/commands.md b/docs/commands.md
index 72ca828c..f66c8324 100644
--- a/docs/commands.md
+++ b/docs/commands.md
@@ -29,7 +29,7 @@ of kinds, keys and error codes.
| Help | [`kinds`, `guide`](#kinds-and-guide) | reference tables, cheat sheet for agents |
`-` as the file reads the model from stdin; a mutating command then writes to stdout. Ids, kinds and
-keys are in [Ids, kinds and keys](reference.md).
+keys are in [Syntax and output](reference.md).
## Options of every write
@@ -83,8 +83,10 @@ Creates one element and wires it in. A placement option says where; trailing `ke
like `set`.
```sh
-bpmn add order.bpmn userTask "Check invoice" --after Event_OrderReceived camunda:assignee=kermit
-bpmn add order.bpmn boundary:timer "2 days" --on Activity_CheckInvoice --timer PT2D --non-interrupting
+bpmn add order.bpmn userTask "Check invoice" --after Event_OrderReceived \
+ camunda:assignee=kermit
+bpmn add order.bpmn boundary:timer "2 days" --on Activity_CheckInvoice \
+ --timer PT2D --non-interrupting
```
- Placement: `--after`, `--before`, `--flow`, `--in`, `--on`, `--lane` ([Placement grammar](reference.md#placement-grammar)).
@@ -107,8 +109,10 @@ The endpoints decide the kind: sequence flow, message flow (different pools), as
## `set`
```sh
-bpmn set order.bpmn Activity_CheckInvoice name="Check the invoice" camunda:asyncBefore=true
-bpmn set order.bpmn Activity_CheckInvoice 'loop.camunda:collection=${invoices}'
+bpmn set order.bpmn Activity_CheckInvoice name="Check the invoice" \
+ camunda:asyncBefore=true
+bpmn set order.bpmn Activity_CheckInvoice \
+ 'loop.camunda:collection=${invoices}'
```
`key=` or `--unset key` removes a property. `id=New_Id` renames and re-points every reference. Parts
@@ -144,7 +148,8 @@ A sub-process with content becomes a task only with `--force`.
```sh
bpmn move order.bpmn Activity_ClarifyInvoice --before Activity_BookInvoice
-bpmn order order.bpmn Gateway_InvoiceOk Flow_InvoiceOkToClarifyInvoice Flow_InvoiceOkToBookInvoice
+bpmn order order.bpmn Gateway_InvoiceOk Flow_InvoiceOkToClarifyInvoice \
+ Flow_InvoiceOkToBookInvoice
```
The flow order drives the picture: the default flow (else the first) continues straight, the others
@@ -232,4 +237,4 @@ is its core, `guide ` one part.
- [Ops JSON](ops.md): many changes in one batch.
- [Layout and formatting](layout.md): how the drawing is kept, and the format commands.
-- [Ids, kinds and keys](reference.md): every kind, trigger and key.
+- [Syntax and output](reference.md): every kind, trigger and key.
diff --git a/docs/contract.md b/docs/contract.md
index 81d4eedb..0357f3fb 100644
--- a/docs/contract.md
+++ b/docs/contract.md
@@ -77,5 +77,5 @@ merged file (`E_IMPORT_LOSSY`): rename one of them while resolving the merge.
## Next steps
- [Layout](layout.md): modes, engines and the format commands.
-- [Ids, kinds and keys](reference.md): how ids are made.
+- [Syntax and output](reference.md): how ids are made.
- [bpmnlint](bpmnlint.md): project rules on every write.
diff --git a/docs/library.md b/docs/library.md
index ce758ccd..d9a7f92c 100644
--- a/docs/library.md
+++ b/docs/library.md
@@ -12,22 +12,45 @@ npm install @miragon/bpmn-cli
The functions take the XML as a string and return the new XML with the result the CLI prints:
```ts
-import { applyToXml, CliError, newXml, renderMutation, showXml } from '@miragon/bpmn-cli';
+import {
+ applyToXml,
+ CliError,
+ newXml,
+ renderMutation,
+ showXml,
+} from '@miragon/bpmn-cli';
let { xml } = await newXml({ processName: 'Order handling' });
const edit = await applyToXml(xml, [
{ op: 'add', kind: 'start', name: 'Order received' },
- { op: 'add', kind: 'userTask', name: 'Check invoice', after: 'Event_OrderReceived' },
+ {
+ op: 'add',
+ kind: 'userTask',
+ name: 'Check invoice',
+ after: 'Event_OrderReceived',
+ },
{ op: 'add', kind: 'end', name: 'Done', after: 'Activity_CheckInvoice' },
]);
if (!edit.unchanged) xml = edit.xml; // and save it
-edit.result.created.map((c) => c.id); // ['Event_OrderReceived', 'Activity_CheckInvoice', 'Flow_OrderReceivedToCheckInvoice', 'Event_Done', 'Flow_CheckInvoiceToDone']
-edit.result.layout.mode; // 'full' (no diagram before: drawn from scratch)
-renderMutation(edit.result); // the text `bpmn apply` prints: created ... / layout: ok - full (...)
-await showXml(xml); // the `bpmn show` text of the README quick start
+
+edit.result.created.map((c) => c.id);
+// ['Event_OrderReceived', 'Activity_CheckInvoice',
+// 'Flow_OrderReceivedToCheckInvoice', 'Event_Done',
+// 'Flow_CheckInvoiceToDone']
+
+edit.result.layout.mode;
+// 'full' (no diagram before: drawn from scratch)
+
+renderMutation(edit.result);
+// the text `bpmn apply` prints: created ... / layout: ok - full (...)
+
+await showXml(xml);
+// the `bpmn show` text of the README quick start
try {
- await applyToXml(xml, [{ op: 'add', kind: 'userTask', name: 'Ship', after: 'Activity_Check' }]);
+ await applyToXml(xml, [
+ { op: 'add', kind: 'userTask', name: 'Ship', after: 'Activity_Check' },
+ ]);
} catch (err) {
if (!(err instanceof CliError)) throw err;
err.code; // 'E_NOT_FOUND'
@@ -120,14 +143,22 @@ A bpmnlint configuration as an advisory validator ([Validators](validators.md#ad
In Node `bpmnlintEngine()` gives the engine; a browser host bundles bpmnlint's `Linter` itself.
```ts
-import { applyToXml, bpmnlintValidator, resolveBpmnlintConfig } from '@miragon/bpmn-cli';
+import {
+ applyToXml,
+ bpmnlintValidator,
+ resolveBpmnlintConfig,
+} from '@miragon/bpmn-cli';
import { bpmnlintEngine } from '@miragon/bpmn-cli/node';
const engine = bpmnlintEngine();
-const resolved = await resolveBpmnlintConfig(engine, { extends: 'bpmnlint:recommended' });
+const resolved = await resolveBpmnlintConfig(engine, {
+ extends: 'bpmnlint:recommended',
+});
const lint = bpmnlintValidator(engine, resolved, { detail: 'our rules' });
const edit = await applyToXml(xml, ops, { validators: [lint] });
-edit.result.warnings.added; // the bpmnlint findings the change added, an error with severity 'error'
+
+edit.result.warnings.added;
+// the bpmnlint findings the change added, an error with severity 'error'
```
### Declared id conventions
@@ -137,11 +168,33 @@ conventions, or its lint reports bpmn-cli's new ids. A table of your own works t
([bpmnlint](bpmnlint.md#id-conventions) has the rules):
```ts
-const automation = await resolveBpmnlintConfig(engine, engine.plugin.getDefaultLintConfig({ preset: 'automation' }));
-const { conventions, notes } = idConventionsFromLint(automation.rules[ID_NAMING_RULE], engine.plugin.naming, { source: 'our rules' });
-const named = await applyToXml(xml, ops, { validators: [bpmnlintValidator(engine, automation, { notes })], conventions });
-named.result.conventions; // { ids: [...], source: 'our rules' }: the new ids that follow the rule, not the file's style
-await applyToXml(xml, ops, { conventions: { prefixes: { 'bpmn:ServiceTask': 'serviceTask_', 'bpmn:SequenceFlow': 'flow_' }, case: 'camelCase' } });
+const automation = await resolveBpmnlintConfig(
+ engine,
+ engine.plugin.getDefaultLintConfig({ preset: 'automation' }),
+);
+const { conventions, notes } = idConventionsFromLint(
+ automation.rules[ID_NAMING_RULE],
+ engine.plugin.naming,
+ { source: 'our rules' },
+);
+const named = await applyToXml(xml, ops, {
+ validators: [bpmnlintValidator(engine, automation, { notes })],
+ conventions,
+});
+
+named.result.conventions;
+// { ids: [...], source: 'our rules' }:
+// the new ids that follow the rule, not the file's style
+
+await applyToXml(xml, ops, {
+ conventions: {
+ prefixes: {
+ 'bpmn:ServiceTask': 'serviceTask_',
+ 'bpmn:SequenceFlow': 'flow_',
+ },
+ case: 'camelCase',
+ },
+});
```
## Browser bundles
diff --git a/docs/ops.md b/docs/ops.md
index 784fac02..d99041e1 100644
--- a/docs/ops.md
+++ b/docs/ops.md
@@ -21,12 +21,25 @@ whenever a change touches more than one element.
```json [book.json]
[
- { "op": "add", "kind": "serviceTask", "name": "Book invoice",
- "after": "Activity_CheckInvoice", "as": "$book" },
- { "op": "set", "id": "$book",
- "values": { "doc": "Posts the invoice to the ledger." } },
- { "op": "ext", "id": "$book", "action": "add",
- "type": "zeebe:taskDefinition", "attrs": { "type": "book-invoice" } }
+ {
+ "op": "add",
+ "kind": "serviceTask",
+ "name": "Book invoice",
+ "after": "Activity_CheckInvoice",
+ "as": "$book"
+ },
+ {
+ "op": "set",
+ "id": "$book",
+ "values": { "doc": "Posts the invoice to the ledger." }
+ },
+ {
+ "op": "ext",
+ "id": "$book",
+ "action": "add",
+ "type": "zeebe:taskDefinition",
+ "attrs": { "type": "book-invoice" }
+ }
]
```
@@ -77,7 +90,14 @@ retype. On a splice `"flowAs"` names the flow into the new node and `"outAs"` th
creates (new node -> old successor), so the same batch can give it a condition:
```json
-{"op": "add", "kind": "exclusiveGateway", "name": "Valid?", "flow": "Flow_CheckToOk", "as": "$valid", "outAs": "$yes"}
+{
+ "op": "add",
+ "kind": "exclusiveGateway",
+ "name": "Valid?",
+ "flow": "Flow_CheckToOk",
+ "as": "$valid",
+ "outAs": "$yes"
+}
```
## Flow ids in a batch
@@ -96,38 +116,86 @@ splits after the check, adds a reminder timer with its own path and gives both t
```json [ops.json]
{
"ops": [
- { "op": "split", "after": "Activity_CheckInvoice",
- "kind": "exclusiveGateway", "name": "Invoice ok?", "as": "$ok",
+ {
+ "op": "split",
+ "after": "Activity_CheckInvoice",
+ "kind": "exclusiveGateway",
+ "name": "Invoice ok?",
+ "as": "$ok",
"branches": [
- { "flowName": "yes", "condition": "= ok",
+ {
+ "flowName": "yes",
+ "condition": "= ok",
"nodes": [
- { "kind": "serviceTask", "name": "Book invoice", "as": "$book" }
- ] },
- { "flowName": "no", "default": true,
+ {
+ "kind": "serviceTask",
+ "name": "Book invoice",
+ "as": "$book"
+ }
+ ]
+ },
+ {
+ "flowName": "no",
+ "default": true,
"nodes": [
- { "kind": "userTask", "name": "Clarify invoice", "as": "$clarify",
+ {
+ "kind": "userTask",
+ "name": "Clarify invoice",
+ "as": "$clarify",
"set": {
"doc": "Call the customer and clarify the open positions."
- } }
- ] }
- ] },
- { "op": "add", "kind": "boundaryEvent:timer", "name": "Reminder",
- "on": "$clarify", "timer": "P2D", "nonInterrupting": true,
- "as": "$reminder" },
- { "op": "add", "kind": "sendTask", "name": "Remind customer",
- "after": "$reminder", "as": "$remind" },
- { "op": "add", "kind": "endEvent", "name": "Reminder sent",
- "in": "Process_OrderHandling", "as": "$sent" },
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "op": "add",
+ "kind": "boundaryEvent:timer",
+ "name": "Reminder",
+ "on": "$clarify",
+ "timer": "P2D",
+ "nonInterrupting": true,
+ "as": "$reminder"
+ },
+ {
+ "op": "add",
+ "kind": "sendTask",
+ "name": "Remind customer",
+ "after": "$reminder",
+ "as": "$remind"
+ },
+ {
+ "op": "add",
+ "kind": "endEvent",
+ "name": "Reminder sent",
+ "in": "Process_OrderHandling",
+ "as": "$sent"
+ },
{ "op": "connect", "source": "$remind", "target": "$sent" },
- { "op": "set", "id": "$book",
- "values": { "name": "Book invoice in ERP",
- "doc": "Posts the invoice to the ledger." } },
- { "op": "ext", "id": "$book", "action": "add",
+ {
+ "op": "set",
+ "id": "$book",
+ "values": {
+ "name": "Book invoice in ERP",
+ "doc": "Posts the invoice to the ledger."
+ }
+ },
+ {
+ "op": "ext",
+ "id": "$book",
+ "action": "add",
"type": "zeebe:taskDefinition",
- "attrs": { "type": "book-invoice", "retries": "3" } },
- { "op": "ext", "id": "$remind", "action": "add",
+ "attrs": { "type": "book-invoice", "retries": "3" }
+ },
+ {
+ "op": "ext",
+ "id": "$remind",
+ "action": "add",
"type": "zeebe:taskDefinition",
- "attrs": { "type": "remind-customer" } }
+ "attrs": { "type": "remind-customer" }
+ }
]
}
```
diff --git a/docs/reference.md b/docs/reference.md
index 0559faa5..cb831407 100644
--- a/docs/reference.md
+++ b/docs/reference.md
@@ -1,4 +1,4 @@
-# Ids, kinds and keys
+# Syntax and output
How new ids are made, the element kinds and triggers, where a node goes, the keys `set` takes, and
what a command prints and exits with.
@@ -147,7 +147,10 @@ written: order.bpmn
```json
{
- "ok": true, "file": "order.bpmn", "written": true, "unchanged": false,
+ "ok": true,
+ "file": "order.bpmn",
+ "written": true,
+ "unchanged": false,
"created": [{ "id": "Event_Done", "kind": "endEvent", "name": "Done" }],
"warnings": { "added": [], "resolved": [], "preexistingCount": 0 },
"layout": { "status": "ok", "mode": "full" },
diff --git a/docs/validators.md b/docs/validators.md
index 74dcebb9..72a44efa 100644
--- a/docs/validators.md
+++ b/docs/validators.md
@@ -10,7 +10,12 @@ A house rule: a path ends in an end event. The first write would add a task with
flow and is refused; the second one passes.
```ts
-import { applyToXml, CliError, newXml, type NamedValidator } from '@miragon/bpmn-cli';
+import {
+ applyToXml,
+ CliError,
+ newXml,
+ type NamedValidator,
+} from '@miragon/bpmn-cli';
// a house rule: a path ends in an end event, never in a task or a gateway
const houseRules: NamedValidator = {
@@ -20,8 +25,14 @@ const houseRules: NamedValidator = {
validateDoc: (doc) =>
doc.processes()
.flatMap((process) => doc.flowNodes(process))
- .filter((node) => node.$type !== 'bpmn:EndEvent' && doc.outgoing(node).length === 0)
- .map((node) => ({ severity: 'error', code: 'DEAD_END', message: `${node.id} has no outgoing sequence flow`, element: node.id })),
+ .filter((node) => node.$type !== 'bpmn:EndEvent')
+ .filter((node) => doc.outgoing(node).length === 0)
+ .map((node) => ({
+ severity: 'error',
+ code: 'DEAD_END',
+ message: `${node.id} has no outgoing sequence flow`,
+ element: node.id,
+ })),
};
let { xml } = await newXml({ processName: 'Order handling' });
@@ -31,16 +42,39 @@ let { xml } = await newXml({ processName: 'Order handling' });
]));
try {
- await applyToXml(xml, [{ op: 'add', kind: 'userTask', name: 'Archive', in: 'Process_OrderHandling' }], { validators: [houseRules] });
+ const archive = {
+ op: 'add',
+ kind: 'userTask',
+ name: 'Archive',
+ in: 'Process_OrderHandling',
+ };
+ await applyToXml(xml, [archive], { validators: [houseRules] });
} catch (err) {
if (!(err instanceof CliError)) throw err;
- err.code; // 'E_VALIDATION'
- err.message; // 'The change would introduce 1 error(s) reported by validator house-rules; nothing was written'
- err.details.errors; // [{ code: 'DEAD_END', message: 'Activity_Archive has no outgoing sequence flow', validator: 'house-rules', severity: 'error', element: 'Activity_Archive' }]
+ err.code;
+ // 'E_VALIDATION'
+ err.message;
+ // 'The change would introduce 1 error(s) reported by validator
+ // house-rules; nothing was written'
+ err.details.errors;
+ // [{ code: 'DEAD_END',
+ // message: 'Activity_Archive has no outgoing sequence flow',
+ // validator: 'house-rules', severity: 'error',
+ // element: 'Activity_Archive' }]
}
-const edit = await applyToXml(xml, [{ op: 'add', kind: 'userTask', name: 'Check invoice', after: 'Event_OrderReceived' }], { validators: [houseRules] });
-edit.result.validation.validators; // [{ name: 'house-rules', detail: 'our modelling guideline', errors: [], warnings: [], preexisting: [], resolved: [], counts: { errors: 0, warnings: 0 } }]
+const check = {
+ op: 'add',
+ kind: 'userTask',
+ name: 'Check invoice',
+ after: 'Event_OrderReceived',
+};
+const edit = await applyToXml(xml, [check], { validators: [houseRules] });
+
+edit.result.validation.validators;
+// [{ name: 'house-rules', detail: 'our modelling guideline',
+// errors: [], warnings: [], preexisting: [], resolved: [],
+// counts: { errors: 0, warnings: 0 } }]
```
## Writing a validator
@@ -87,14 +121,26 @@ A validator with `blocks: false` reports and never refuses. The project's bpmnli
([bpmnlint](bpmnlint.md)). A host can run the plugin's default configuration the same way:
```js
-import { applyToXml, bpmnlintValidator, idConventionsFromLint, ID_NAMING_RULE, resolveBpmnlintConfig } from '@miragon/bpmn-cli';
+import {
+ applyToXml,
+ bpmnlintValidator,
+ idConventionsFromLint,
+ ID_NAMING_RULE,
+ resolveBpmnlintConfig,
+} from '@miragon/bpmn-cli';
import { bpmnlintEngine } from '@miragon/bpmn-cli/node';
const engine = bpmnlintEngine();
const rc = engine.plugin.getDefaultLintConfig({ engine: 'c7' });
const resolved = await resolveBpmnlintConfig(engine, rc);
-const { conventions, notes } = idConventionsFromLint(resolved.rules[ID_NAMING_RULE], engine.plugin.naming);
-const lint = bpmnlintValidator(engine, resolved, { detail: 'default rules', notes });
+const { conventions, notes } = idConventionsFromLint(
+ resolved.rules[ID_NAMING_RULE],
+ engine.plugin.naming,
+);
+const lint = bpmnlintValidator(engine, resolved, {
+ detail: 'default rules',
+ notes,
+});
await applyToXml(xml, ops, { validators: [lint], conventions });
```
diff --git a/docs/walkthrough.md b/docs/walkthrough.md
index e38de872..ec3c3825 100644
--- a/docs/walkthrough.md
+++ b/docs/walkthrough.md
@@ -43,20 +43,42 @@ a gateway with its branches and the join; an alias (`"as": "$book"`) names a new
ops.
```json [ops.json]
-{ "ops": [
- { "op": "split", "after": "Activity_CheckInvoice", "name": "Invoice ok?",
- "branches": [
- { "flowName": "yes", "condition": "= ok",
- "nodes": [
- { "kind": "serviceTask", "name": "Book invoice", "as": "$book" }
- ] },
- { "flowName": "no", "default": true,
- "nodes": [
- { "kind": "sendTask", "name": "Ask customer", "as": "$ask" }
- ] } ] },
- { "op": "ext", "id": "$book", "action": "add",
- "type": "zeebe:taskDefinition", "attrs": { "type": "book-invoice" } }
-] }
+{
+ "ops": [
+ {
+ "op": "split",
+ "after": "Activity_CheckInvoice",
+ "name": "Invoice ok?",
+ "branches": [
+ {
+ "flowName": "yes",
+ "condition": "= ok",
+ "nodes": [
+ {
+ "kind": "serviceTask",
+ "name": "Book invoice",
+ "as": "$book"
+ }
+ ]
+ },
+ {
+ "flowName": "no",
+ "default": true,
+ "nodes": [
+ { "kind": "sendTask", "name": "Ask customer", "as": "$ask" }
+ ]
+ }
+ ]
+ },
+ {
+ "op": "ext",
+ "id": "$book",
+ "action": "add",
+ "type": "zeebe:taskDefinition",
+ "attrs": { "type": "book-invoice" }
+ }
+ ]
+}
```
```console{4,5}
diff --git a/test/adapters/out/files/bpmnlint.test.ts b/test/adapters/out/files/bpmnlint.test.ts
index e480930b..d9e87971 100644
--- a/test/adapters/out/files/bpmnlint.test.ts
+++ b/test/adapters/out/files/bpmnlint.test.ts
@@ -83,6 +83,9 @@ const listless = (xml: string): string => xml.replace(/\s* (issues as ValidatorIssue[]).filter((warning) => warning.validator === 'bpmnlint');
const codes = (issues: readonly unknown[]): string[] => (issues as Array<{ code: string; element?: string }>).map((warning) => `${warning.code}${warning.element ? ` ${warning.element}` : ''}`);
+/** A code example as written in the docs, without line breaks, indentation, comment markers or trailing commas. */
+const codeOf = (text: string): string => text.replace(/\/\/|\s+/g, '').replace(/,(?=[)\]}])/g, '');
+
describe('resolveBpmnlintConfig: the effective rules', () => {
it('normalises the names, keeps the options as bpmnlint hands them to the rule, runs any flag but off as a warning', async () => {
const engine = bpmnlintEngine();
@@ -1702,7 +1705,7 @@ describe('the library examples (docs/library.md, docs/validators.md)', () => {
"const resolved = await resolveBpmnlintConfig(engine, { extends: 'bpmnlint:recommended' });",
"const lint = bpmnlintValidator(engine, resolved, { detail: 'our rules' });",
'const edit = await applyToXml(xml, ops, { validators: [lint] });',
- ]) expect(library).toContain(line);
+ ]) expect(codeOf(library)).toContain(codeOf(line));
const engine = bpmnlintEngine();
const resolved = await resolveBpmnlintConfig(engine, { extends: 'bpmnlint:recommended' });
const lint = bpmnlintValidator(engine, resolved, { detail: 'our rules' });
@@ -1716,7 +1719,7 @@ describe('the library examples (docs/library.md, docs/validators.md)', () => {
'const { conventions, notes } = idConventionsFromLint(resolved.rules[ID_NAMING_RULE], engine.plugin.naming);',
"const lint = bpmnlintValidator(engine, resolved, { detail: 'default rules', notes });",
'await applyToXml(xml, ops, { validators: [lint], conventions });',
- ]) expect(validators).toContain(line);
+ ]) expect(codeOf(validators)).toContain(codeOf(line));
for (const target of ['c7', 'c8'] as const) {
const rc = engine.plugin.getDefaultLintConfig({ engine: target });
const resolved = await resolveBpmnlintConfig(engine, rc);
@@ -1746,7 +1749,7 @@ describe('the library examples (docs/library.md, docs/validators.md)', () => {
"const { conventions, notes } = idConventionsFromLint(automation.rules[ID_NAMING_RULE], engine.plugin.naming, { source: 'our rules' });",
'const named = await applyToXml(xml, ops, { validators: [bpmnlintValidator(engine, automation, { notes })], conventions });',
"await applyToXml(xml, ops, { conventions: { prefixes: { 'bpmn:ServiceTask': 'serviceTask_', 'bpmn:SequenceFlow': 'flow_' }, case: 'camelCase' } });",
- ]) expect(library).toContain(line);
+ ]) expect(codeOf(library)).toContain(codeOf(line));
const engine = bpmnlintEngine();
const automation = await resolveBpmnlintConfig(engine, engine.plugin.getDefaultLintConfig({ preset: 'automation' }));
const { conventions, notes } = idConventionsFromLint(automation.rules[ID_NAMING_RULE], engine.plugin.naming, { source: 'our rules' });
diff --git a/test/e2e/api.test.ts b/test/e2e/api.test.ts
index 6de52a47..de75ea9c 100644
--- a/test/e2e/api.test.ts
+++ b/test/e2e/api.test.ts
@@ -49,6 +49,9 @@ interface Run {
err: string;
}
+/** A code example as written in the docs, without line breaks, indentation, comment markers or trailing commas. */
+const codeOf = (text: string): string => text.replace(/\/\/|\s+/g, '').replace(/,(?=[)\]}])/g, '');
+
function cli(...args: string[]): Run {
const spawned = spawnSync(process.execPath, ['--import', 'tsx', join(ROOT, 'src', 'cli.ts'), ...args], { encoding: 'utf8', cwd: ROOT, input: '' });
return { code: spawned.status ?? -1, out: spawned.stdout, err: spawned.stderr };
@@ -84,7 +87,7 @@ describe('the docs/library.md example (Library use)', () => {
expect(example).toContain("{ op: 'add', kind: 'end', name: 'Done', after: 'Activity_CheckInvoice' },");
if (!edit.unchanged) xml = edit.xml;
const quote = (data: unknown): string => JSON.stringify(data).replaceAll('"', "'").replaceAll(',', ', ');
- expect(example).toContain(`// ${quote(edit.result.created.map((change) => change.id))}`);
+ expect(codeOf(example)).toContain(codeOf(quote(edit.result.created.map((change) => change.id))));
expect(example).toContain(`// '${edit.result.layout.mode}' (${edit.result.layout.reason})`);
expect(renderMutation(edit.result)).toMatch(/^created startEvent [\s\S]*\nlayout: ok - full \(/m);
const quickStart = /\$ bpmn show order\.bpmn\n([\s\S]*?)\n```/.exec(readme)?.[1];