diff --git a/GLOSSARY.md b/GLOSSARY.md
index 5cf4b573..09fbd483 100644
--- a/GLOSSARY.md
+++ b/GLOSSARY.md
@@ -9,7 +9,7 @@ a new concept gets its line here first.
| **Edit command** | One change a caller asks for, named after what it does: a model command (`AddElement`, `ConnectElements`, `SetProperties`, `RemoveElements`, `ChangeElementType`, `MoveElements`, `ReorderElements`, `EditExtensionElements`, `SplitIntoBranches`) or a diagram command (`PlaceShapes`, `AlignShapes`, `ColorElements`, `PlaceLabel`, `RerouteFlow`, `InsertSpace`, `TidyShapes`, `CompactDiagram`); the ops JSON of `apply` spells them as ops (`"op": "add"`) (`domain/commands/types.ts`). |
| **Use case** | The contract of one command line verb or API function: an interface with its `Command` and `Result` in `application/port/in/` (`EditDocumentUseCase`, `CreateDocumentUseCase`, `RelayoutDiagramUseCase`, `ValidateDocumentUseCase`, `ReadDocumentUseCase`, `ShowModelUseCase`, `ShowConventionsUseCase`, `FindElementsUseCase`, `RenderDiagramUseCase`, `RenderSvgUseCase`, `RunCommandsUseCase`, `AnalyzeLayoutUseCase`), implemented by the service of the same name in `application/service/` (`EditDocumentService`, ...). Every public function of the library that works on a document goes through one, even where the service only forwards to the domain. |
| **Service** | A class named `...Service`: in the domain, work the commands share or run (`ElementCreationService`, `SequenceFlowService`, ...); in `application/service/`, the implementation of one use case. |
-| **Port** / **adapter** | The way in (`application/port/in/`, the use cases) and out (`application/port/out/`: `LoadDocumentPort`, `SaveDocumentPort`, `LoadProjectSettingsPort`, `WriteImagePort`) / what drives the application (`adapters/in/cli/`) or provides an outbound port (`adapters/out/files/` on disk, `adapters/out/memory/` for the in-memory API); only the entries wire them together. |
+| **Port** / **adapter** | The way in (`application/port/in/`, the use cases) and out (`application/port/out/`: `LoadDocumentPort`, `SaveDocumentPort`, `LoadProjectSettingsPort`, `WriteImagePort`) / what drives the application (`adapters/in/cli/`) or provides an outbound port (`adapters/out/files/` on disk, `adapters/out/memory/` for the in-memory API, which has no project settings and passes no document ports); only the entries wire them together. |
| **Batch** | The commands of one `apply` call (or of one changing command), run on the document in memory in their order: a diagram command runs after the layout of the model commands before it. Guard, commands, validation, layout, validators, result, all or nothing (`domain/batch/EditBatchService.ts`); the use case then saves the result through `SaveDocumentPort`. |
| **Edit context** | The state of one edit: the call's settings (id conventions, platform, kept flow ids) and what the running batch recorded (`domain/document/EditContext.ts`). The editing services receive it next to the document; `new EditContext(doc)` starts a fresh one. |
| **Model command service** | Hands each model command of a batch to its domain service in an edit context; a batch runs in segments, the drawing brought up to date between them (`domain/batch/ModelCommandService.ts`). |
diff --git a/docs/architecture.md b/docs/architecture.md
index 7c79498d..eca263d6 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -23,7 +23,7 @@ flowchart TB
service --> domain["Domain
domain/"]
service --> portOut{{"Port
application/port/out"}}
portOut --> files["Files
adapters/out/files"]
- portOut --> memory["Memory
adapters/out/memory"]
+ portOut --> memory["No project
adapters/out/memory"]
classDef adapter stroke:#2f80ed,stroke-width:2px
classDef app stroke:#f2994a,stroke-width:2px
@@ -42,7 +42,7 @@ 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 |
+| `application/` imports no adapter | a service does not know whether it writes to disk or does not write at all |
| 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 |
@@ -158,22 +158,23 @@ export declare namespace EditDocumentUseCase {
### 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.
+`settings`, `load` and `save` are outbound ports, handed in by the constructor; without `load`
+or `save` the service takes only a document and writes nothing.
```ts
// src/application/service/EditDocumentService.ts
export class EditDocumentService implements EditDocumentUseCase {
constructor(
- private readonly load: LoadDocumentPort,
- private readonly save: SaveDocumentPort,
private readonly settings: LoadProjectSettingsPort,
+ private readonly load?: LoadDocumentPort,
+ private readonly save?: SaveDocumentPort,
) {}
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)
+ : await Doc.fromXml(await this.loadPort(source.file).read(source.file), source.file)
const target = opts.out ?? doc.file
// the project's .bpmnlintrc, if any
@@ -186,15 +187,15 @@ export class EditDocumentService implements EditDocumentUseCase {
// step 7: write the file
if (!opts.dryRun) {
- result.written = await this.save.save(target, result.xml, { … })
+ result.written = await this.savePort().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.
+In the CLI the ports are a `FileDocumentStore` and `BpmnlintProjectSettings`; `applyToXml()`
+passes only `NoProjectSettings` and a `{ doc }` with `dryRun`, so nothing is read or written.
### 4. The batch
@@ -332,7 +333,7 @@ written: order.bpmn
- `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
+- `applyToXml()` (`api.ts`) starts at step 2, with nothing 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.
diff --git a/src/adapters/in/cli/NewFileCommand.ts b/src/adapters/in/cli/NewFileCommand.ts
index 651c7eb9..9a7d57ae 100644
--- a/src/adapters/in/cli/NewFileCommand.ts
+++ b/src/adapters/in/cli/NewFileCommand.ts
@@ -1,5 +1,6 @@
import { InvalidArgumentError, type Command as CommandType } from 'commander';
import { Doc } from '../../../domain/document/Doc.js';
+import type { Target } from '../../../domain/document/vendor/PlatformDetection.js';
import type { RawOpts } from './CliCommand.js';
import { MutationCommand } from './MutationCommand.js';
@@ -35,7 +36,7 @@ export class NewFileCommand extends MutationCommand {
processName: options['name'] as string | undefined,
processId: options['id'] as string | undefined,
executable: options['executable'] !== false,
- target: options['target'] as 'camunda8' | 'camunda7' | undefined,
+ target: options['target'] as Target | undefined,
mustNotExist: true,
...(stdout ? { out: undefined, dryRun: true } : {}),
});
diff --git a/src/adapters/out/memory/InMemoryDocumentStore.ts b/src/adapters/out/memory/InMemoryDocumentStore.ts
deleted file mode 100644
index 63779414..00000000
--- a/src/adapters/out/memory/InMemoryDocumentStore.ts
+++ /dev/null
@@ -1,31 +0,0 @@
-import type { LoadDocumentPort } from '../../../application/port/out/LoadDocumentPort.js';
-import type { SaveDocumentPort } from '../../../application/port/out/SaveDocumentPort.js';
-import { ioError } from '../../../domain/document/utils/errors.js';
-
-/** Documents as texts by name, for hosts without files (the browser, the in-memory API). */
-export class InMemoryDocumentStore implements LoadDocumentPort, SaveDocumentPort {
- private readonly texts = new Map();
-
- async read(file: string): Promise {
- const text = this.texts.get(file);
- if (text === undefined) throw ioError('E_FILE_NOT_FOUND', `Cannot read ${file}: no such document`, { file });
- return text;
- }
-
- decode(bytes: Uint8Array): string {
- return new TextDecoder('utf-8').decode(bytes);
- }
-
- async save(target: string, xml: string, opts: { source?: string; unchanged: boolean; backup?: boolean }): Promise {
- if (opts.unchanged && opts.source === target) return false;
- const original = opts.backup && opts.source ? this.texts.get(opts.source) : undefined;
- if (original !== undefined) this.texts.set(`${opts.source}.bak`, original);
- this.texts.set(target, xml);
- return true;
- }
-
- async assertNew(target: string | undefined, opts: { force?: boolean; dryRun?: boolean }): Promise {
- if (!target || opts.force || opts.dryRun || !this.texts.has(target)) return;
- throw ioError('E_FILE_EXISTS', `${target} already exists`, { file: target, hint: 'Choose another name or pass force to overwrite.' });
- }
-}
diff --git a/src/api.ts b/src/api.ts
index 2257d367..96fd504a 100644
--- a/src/api.ts
+++ b/src/api.ts
@@ -19,7 +19,6 @@ import type { LayoutMode } from './domain/diagram/types.js';
import type { CheckOptions, CheckResult } from './application/port/in/ValidateDocumentUseCase.js';
import type { LayoutDocOptions } from './application/port/in/RelayoutDiagramUseCase.js';
import { EditDocumentService } from './application/service/EditDocumentService.js';
-import { InMemoryDocumentStore } from './adapters/out/memory/InMemoryDocumentStore.js';
import { NoProjectSettings } from './adapters/out/memory/NoProjectSettings.js';
import { RelayoutDiagramService } from './application/service/RelayoutDiagramService.js';
import { ValidateDocumentService } from './application/service/ValidateDocumentService.js';
@@ -41,9 +40,8 @@ import { MutationReportBuilder } from './adapters/in/report/MutationReportBuilde
import { ShownText } from './adapters/in/report/ShownText.js';
/** The in-memory API runs the use cases without files and without a project around the document. */
-const IN_MEMORY = new InMemoryDocumentStore();
const NO_PROJECT = new NoProjectSettings();
-const READ_DOCUMENT = new ReadDocumentService(IN_MEMORY);
+const READ_DOCUMENT = new ReadDocumentService();
export type { ShownView, ViewOptions } from './domain/views/ViewRequest.js';
@@ -104,12 +102,12 @@ function parseXml(xml: string, file?: string): Promise {
export async function applyToXml(xml: string, ops: unknown, opts: EditOptions = {}): Promise {
const list = readOps(ops);
const doc = await loadXml(xml, opts);
- return toEditResult(xml, await new EditDocumentService(IN_MEMORY, IN_MEMORY, NO_PROJECT).edit({ ...opts, source: { doc }, commands: list, dryRun: true }));
+ return toEditResult(xml, await new EditDocumentService(NO_PROJECT).edit({ ...opts, source: { doc }, commands: list, dryRun: true }));
}
/** `bpmn new`: a laid-out document with one empty process. */
export async function newXml(opts: NewDocOptions & EditOptions = {}): Promise {
- return toEditResult(undefined, await new CreateDocumentService(IN_MEMORY, NO_PROJECT).create({ ...opts, dryRun: true }));
+ return toEditResult(undefined, await new CreateDocumentService(NO_PROJECT).create({ ...opts, dryRun: true }));
}
export interface LayoutXmlOptions extends Omit {
@@ -123,7 +121,7 @@ export interface LayoutXmlOptions extends Omit {
export async function layoutXml(xml: string, opts: LayoutXmlOptions = {}): Promise {
RelayoutDiagramService.assertOptions(opts);
const doc = await loadXml(xml, opts);
- return toEditResult(xml, await new RelayoutDiagramService(IN_MEMORY, IN_MEMORY, NO_PROJECT).relayout({ ...opts, source: { doc }, dryRun: true }));
+ return toEditResult(xml, await new RelayoutDiagramService(NO_PROJECT).relayout({ ...opts, source: { doc }, dryRun: true }));
}
export interface ValidateXmlOptions {
@@ -136,7 +134,7 @@ export interface ValidateXmlOptions {
/** `bpmn validate --json`: structure, lint, platform profile, validators and a layout dry run. */
export async function validateXml(xml: string, opts: ValidateXmlOptions = {}): Promise {
- return new ValidationReportBuilder(await new ValidateDocumentService(IN_MEMORY, NO_PROJECT).validate({ ...opts, source: { doc: await parseXml(xml, opts.file) } })).report();
+ return new ValidationReportBuilder(await new ValidateDocumentService(NO_PROJECT).validate({ ...opts, source: { doc: await parseXml(xml, opts.file) } })).report();
}
/** `bpmn show` on a parsed document. */
@@ -187,20 +185,20 @@ export async function extensionsXml(xml: string, id: string): Promise {
- return new EditDocumentService(IN_MEMORY, IN_MEMORY, NO_PROJECT).edit({ ...opts, source: { doc }, commands: ops, dryRun: true });
+ return new EditDocumentService(NO_PROJECT).edit({ ...opts, source: { doc }, commands: ops, dryRun: true });
}
/** `bpmn layout` on an in-memory document. */
export function layoutDoc(doc: Doc, opts: LayoutDocOptions = {}): Promise {
- return new RelayoutDiagramService(IN_MEMORY, IN_MEMORY, NO_PROJECT).relayout({ ...opts, source: { doc }, dryRun: true });
+ return new RelayoutDiagramService(NO_PROJECT).relayout({ ...opts, source: { doc }, dryRun: true });
}
/** What `bpmn validate` finds in a document. */
export function checkDoc(doc: Doc, opts: CheckOptions = {}): Promise {
- return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).validate({ ...opts, source: { doc } });
+ return new ValidateDocumentService(NO_PROJECT).validate({ ...opts, source: { doc } });
}
diff --git a/src/application/service/CreateDocumentService.ts b/src/application/service/CreateDocumentService.ts
index 992bdf99..03f119a4 100644
--- a/src/application/service/CreateDocumentService.ts
+++ b/src/application/service/CreateDocumentService.ts
@@ -8,22 +8,27 @@ import { EditBatchService } from '../../domain/batch/EditBatchService.js';
export class CreateDocumentService implements CreateDocumentUseCase {
constructor(
- private readonly save: SaveDocumentPort,
private readonly settings: LoadProjectSettingsPort,
+ private readonly save?: SaveDocumentPort,
) {}
+ private savePort(): SaveDocumentPort {
+ if (!this.save) throw ioError('E_NO_FILE', 'No output file given');
+ return this.save;
+ }
+
/** An existing file fails before the .bpmnlintrc is read. */
async create(command: CreateDocumentUseCase.Command): Promise {
const { destination, ...opts } = command;
if (opts.processId !== undefined) Doc.assertProcessId(opts.processId);
const target = opts.out ?? destination;
if (!opts.dryRun && !target) throw ioError('E_NO_FILE', 'No output file given');
- if (opts.mustNotExist) await this.save.assertNew(target, opts);
+ if (opts.mustNotExist) await this.save?.assertNew(target, opts);
const lint = await this.settings.lint(opts.file ?? target, opts);
const conventions = opts.conventions ?? (await this.settings.idConventions(target, opts))?.conventions;
const doc = Doc.create({ processName: opts.processName, processId: opts.processId, executable: opts.executable, target: opts.target, ...(conventions ? { conventions } : {}) }, destination);
const result = await new EditBatchService(doc, [], lint.applyTo(opts, target)).run(target);
- if (!opts.dryRun && target) result.written = await this.save.save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
+ if (!opts.dryRun && target) result.written = await this.savePort().save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
result.changes.create(newProcessChange(doc, opts.processName));
return result;
}
diff --git a/src/application/service/EditDocumentService.ts b/src/application/service/EditDocumentService.ts
index a454001b..64a4ac9e 100644
--- a/src/application/service/EditDocumentService.ts
+++ b/src/application/service/EditDocumentService.ts
@@ -9,21 +9,31 @@ import { TextPreservation, type PreservedText } from '../../domain/document/xml/
export class EditDocumentService implements EditDocumentUseCase {
constructor(
- private readonly load: LoadDocumentPort,
- private readonly save: SaveDocumentPort,
private readonly settings: LoadProjectSettingsPort,
+ private readonly load?: LoadDocumentPort,
+ private readonly save?: SaveDocumentPort,
) {}
+ private loadPort(file: string): LoadDocumentPort {
+ if (!this.load) throw ioError('E_FILE_NOT_FOUND', `Cannot read ${file}: no such document`, { file });
+ return this.load;
+ }
+
+ private savePort(): SaveDocumentPort {
+ if (!this.save) throw ioError('E_NO_FILE', 'No output file given');
+ return this.save;
+ }
+
async edit(command: EditDocumentUseCase.Command): Promise {
const { source, commands, ...opts } = command;
- const doc = 'doc' in source ? source.doc : await Doc.fromXml(await this.load.read(source.file), source.file);
+ const doc = 'doc' in source ? source.doc : await Doc.fromXml(await this.loadPort(source.file).read(source.file), source.file);
doc.assertLossless(opts);
const target = opts.out ?? doc.file;
if (!opts.dryRun && !target) throw ioError('E_NO_FILE', 'No output file given');
- if (opts.mustNotExist) await this.save.assertNew(target, opts);
+ if (opts.mustNotExist) await this.save?.assertNew(target, opts);
const lint = await this.settings.lint(opts.file ?? target, opts);
const result = await new EditBatchService(doc, commands, lint.applyTo(opts, target)).run(target);
- if (!opts.dryRun && target) result.written = await this.save.save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
+ if (!opts.dryRun && target) result.written = await this.savePort().save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
return result;
}
diff --git a/src/application/service/ReadDocumentService.ts b/src/application/service/ReadDocumentService.ts
index 6ab1bfbe..840bb437 100644
--- a/src/application/service/ReadDocumentService.ts
+++ b/src/application/service/ReadDocumentService.ts
@@ -1,10 +1,17 @@
import { Doc } from '../../domain/document/Doc.js';
import type { ReadDocumentUseCase } from '../port/in/ReadDocumentUseCase.js';
+import { ioError } from '../../domain/document/utils/errors.js';
import type { LoadDocumentPort } from '../port/out/LoadDocumentPort.js';
/** Decoded in the encoding the XML declaration names, so ISO-8859-1 and Windows-1252 characters survive. */
export class ReadDocumentService implements ReadDocumentUseCase {
- constructor(private readonly load: LoadDocumentPort) {}
+ constructor(private readonly load?: LoadDocumentPort) {}
+
+ private loadPort(file: string): LoadDocumentPort {
+ if (!this.load) throw ioError('E_FILE_NOT_FOUND', `Cannot read ${file}: no such document`, { file });
+ return this.load;
+ }
+
async read(command: ReadDocumentUseCase.Command): Promise {
const doc = await this.parse(command.source);
@@ -18,6 +25,6 @@ export class ReadDocumentService implements ReadDocumentUseCase {
private async parse(source: ReadDocumentUseCase.Command['source']): Promise {
if ('xml' in source) return Doc.fromXml(source.xml, source.file);
- return 'file' in source ? Doc.fromXml(await this.load.read(source.file), source.file) : Doc.fromXml(this.load.decode(source.bytes, source.name));
+ return 'file' in source ? Doc.fromXml(await this.loadPort(source.file).read(source.file), source.file) : Doc.fromXml(this.loadPort(source.name).decode(source.bytes, source.name));
}
}
diff --git a/src/application/service/RelayoutDiagramService.ts b/src/application/service/RelayoutDiagramService.ts
index ba556be7..0366fe9b 100644
--- a/src/application/service/RelayoutDiagramService.ts
+++ b/src/application/service/RelayoutDiagramService.ts
@@ -15,19 +15,29 @@ import type { SaveDocumentPort } from '../port/out/SaveDocumentPort.js';
/** Ids that are no sub-process are errors, not silent no-ops. */
export class RelayoutDiagramService implements RelayoutDiagramUseCase {
constructor(
- private readonly load: LoadDocumentPort,
- private readonly save: SaveDocumentPort,
private readonly settings: LoadProjectSettingsPort,
+ private readonly load?: LoadDocumentPort,
+ private readonly save?: SaveDocumentPort,
) {}
+ private loadPort(file: string): LoadDocumentPort {
+ if (!this.load) throw ioError('E_FILE_NOT_FOUND', `Cannot read ${file}: no such document`, { file });
+ return this.load;
+ }
+
+ private savePort(): SaveDocumentPort {
+ if (!this.save) throw ioError('E_NO_FILE', 'No output file given');
+ return this.save;
+ }
+
async relayout(command: RelayoutDiagramUseCase.Command): Promise {
const { source, ...opts } = command;
RelayoutDiagramService.assertOptions(opts);
- const doc = 'doc' in source ? source.doc : await Doc.fromXml(await this.load.read(source.file), source.file);
+ const doc = 'doc' in source ? source.doc : await Doc.fromXml(await this.loadPort(source.file).read(source.file), source.file);
doc.assertLossless(opts);
const target = opts.out ?? doc.file;
if (!opts.dryRun && !target) throw ioError('E_NO_FILE', 'No output file given');
- if (opts.mustNotExist) await this.save.assertNew(target, opts);
+ if (opts.mustNotExist) await this.save?.assertNew(target, opts);
const project = (await this.settings.lint(opts.file ?? target, opts)).applyTo(opts, target);
if (project.tidy) return this.write(doc, new EditBatchService(doc, [{ op: 'tidy' }], { ...project, layout: 'auto' }), target, opts);
return this.redraw(doc, project, target, opts);
@@ -66,7 +76,7 @@ export class RelayoutDiagramService implements RelayoutDiagramUseCase {
private async write(doc: Doc, batch: EditBatchService, target: string | undefined, opts: FileWriteOptions): Promise {
const result = await batch.run(target);
- if (!opts.dryRun && target) result.written = await this.save.save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
+ if (!opts.dryRun && target) result.written = await this.savePort().save(target, result.xml, { source: doc.file, unchanged: result.unchanged, backup: opts.backup });
return result;
}
}
diff --git a/src/application/service/ValidateDocumentService.ts b/src/application/service/ValidateDocumentService.ts
index f49006de..2e3d2c24 100644
--- a/src/application/service/ValidateDocumentService.ts
+++ b/src/application/service/ValidateDocumentService.ts
@@ -16,19 +16,25 @@ import { once } from '../../domain/validation/utils/once.js';
import type { ValidatorReport, ValidatorRun } from '../../domain/validation/validators.js';
import { toCliError } from '../../domain/document/utils/cli-error.js';
import type { CheckOptions, CheckResult, ValidateDocumentUseCase } from '../port/in/ValidateDocumentUseCase.js';
+import { ioError } from '../../domain/document/utils/errors.js';
import type { LoadDocumentPort } from '../port/out/LoadDocumentPort.js';
import type { LoadProjectSettingsPort } from '../port/out/LoadProjectSettingsPort.js';
/** The layout dry run redraws the in-memory document: pass a document of its own. */
export class ValidateDocumentService implements ValidateDocumentUseCase {
constructor(
- private readonly load: LoadDocumentPort,
private readonly settings: LoadProjectSettingsPort,
+ private readonly load?: LoadDocumentPort,
) {}
+ private loadPort(file: string): LoadDocumentPort {
+ if (!this.load) throw ioError('E_FILE_NOT_FOUND', `Cannot read ${file}: no such document`, { file });
+ return this.load;
+ }
+
async validate(command: ValidateDocumentUseCase.Command): Promise {
const { source, ...opts } = command;
- const doc = 'doc' in source ? source.doc : await Doc.fromXml(await this.load.read(source.file), source.file);
+ const doc = 'doc' in source ? source.doc : await Doc.fromXml(await this.loadPort(source.file).read(source.file), source.file);
return this.check(doc, (await this.settings.lint(opts.file ?? doc.file, opts)).applyTo(opts, doc.file));
}
diff --git a/src/cli.ts b/src/cli.ts
index dfbd9b8d..c6e7a232 100644
--- a/src/cli.ts
+++ b/src/cli.ts
@@ -38,10 +38,10 @@ function exitWhenWritten(): void {
const files = new FileDocumentStore();
const project = new BpmnlintProjectSettings();
const cli = new Cli(VERSION, {
- editDocument: new EditDocumentService(files, files, project),
- createDocument: new CreateDocumentService(files, project),
- relayoutDiagram: new RelayoutDiagramService(files, files, project),
- validateDocument: new ValidateDocumentService(files, project),
+ editDocument: new EditDocumentService(project, files, files),
+ createDocument: new CreateDocumentService(project, files),
+ relayoutDiagram: new RelayoutDiagramService(project, files, files),
+ validateDocument: new ValidateDocumentService(project, files),
readDocument: new ReadDocumentService(files),
showModel: new ShowModelService(),
showConventions: new ShowConventionsService(project),
diff --git a/src/domain/document/Doc.ts b/src/domain/document/Doc.ts
index 1502e966..c9ffdfe2 100644
--- a/src/domain/document/Doc.ts
+++ b/src/domain/document/Doc.ts
@@ -16,7 +16,7 @@ import { createDefinitions, createModdle, indexById, is, isBpmnElement, many, pa
import { ModelError } from './ModelError.js';
import { C7_DEFAULT_TTL, C7_PLATFORM_VERSION } from './vendor/CamundaDescriptor.js';
import { C8_PLATFORM_VERSION } from './vendor/ZeebeDescriptor.js';
-import { PlatformDetection } from './vendor/PlatformDetection.js';
+import { PlatformDetection, platformOfTarget, TARGETS, type Platform, type Target } from './vendor/PlatformDetection.js';
/** `operaton` is Operaton's copy of the camunda namespace (only Operaton reads it). */
export const KNOWN_NAMESPACES: Record = {
@@ -37,13 +37,11 @@ export interface NewDocOptions {
processName?: string;
executable?: boolean;
/** declares vendor namespaces, the platform and (camunda7) the Modeler's process defaults */
- target?: 'camunda8' | 'camunda7' | 'none';
+ target?: Target;
/** process and definitions ids follow them only where they name that type exactly */
conventions?: IdConventions;
}
-export const TARGETS = ['camunda8', 'camunda7', 'none'] as const;
-
export interface DocSource {
text: string;
/** the incoming / outgoing lists as read, before the in-memory repair */
@@ -142,12 +140,13 @@ export class Doc {
});
const doc = new Doc({ moddle, definitions, importWarnings: [] }, ids, file);
if (opts.conventions) doc.conventions = opts.conventions;
- if (opts.target === 'camunda8') {
+ const platform = platformOfTarget(opts.target);
+ if (platform === 'c8') {
doc.declareNamespace('zeebe');
doc.declareNamespace('modeler');
definitions.$attrs['modeler:executionPlatform'] = 'Camunda Cloud';
definitions.$attrs['modeler:executionPlatformVersion'] = C8_PLATFORM_VERSION;
- } else if (opts.target === 'camunda7') {
+ } else if (platform === 'c7') {
doc.declareNamespace('camunda');
doc.declareNamespace('modeler');
definitions.$attrs['modeler:executionPlatform'] = 'Camunda Platform';
@@ -157,8 +156,8 @@ export class Doc {
return doc;
}
- platform(): 'camunda7' | 'camunda8' | undefined {
- return new PlatformDetection(this.definitions).target();
+ platform(): Platform {
+ return new PlatformDetection(this.definitions).info().platform;
}
/**
@@ -167,7 +166,7 @@ export class Doc {
* Operaton-only files). An existing value is never touched.
*/
initProcess(process: El, platform = this.platform()): void {
- if (platform !== 'camunda7') return;
+ if (platform !== 'c7') return;
const camunda = KNOWN_NAMESPACES['camunda']!;
const operaton = KNOWN_NAMESPACES['operaton']!;
const uri = !this.prefixFor(camunda) && this.prefixFor(operaton) ? operaton : camunda;
diff --git a/src/domain/document/EditContext.ts b/src/domain/document/EditContext.ts
index 0438b479..333ff966 100644
--- a/src/domain/document/EditContext.ts
+++ b/src/domain/document/EditContext.ts
@@ -4,6 +4,7 @@ import { ChangeSet, type Rename } from './ChangeSet.js';
import type { Doc } from './Doc.js';
import { IdAllocator } from './IdAllocator.js';
import type { El } from './utils/moddle.js';
+import type { Platform } from './vendor/PlatformDetection.js';
/** What a running `apply` batch did so far: E_NOT_FOUND points to it. */
export interface BatchContext {
@@ -50,7 +51,7 @@ export interface DroppedContent {
export interface EditSettings {
conventions?: IdConventions;
/** so the ops follow the same engine as the profile that reports on them */
- platformChoice?: 'c7' | 'c8' | 'none';
+ platformChoice?: Platform;
keepFlowIds?: boolean;
}
@@ -82,10 +83,8 @@ export class EditContext {
}
/** The call's platform choice, else the document's detected one. */
- platform(): 'camunda7' | 'camunda8' | undefined {
- const choice = this.settings.platformChoice;
- if (choice) return choice === 'c7' ? 'camunda7' : choice === 'c8' ? 'camunda8' : undefined;
- return this.doc.platform();
+ platform(): Platform {
+ return this.settings.platformChoice ?? this.doc.platform();
}
/** Like Doc.require; inside a batch E_NOT_FOUND points to what the batch did. */
diff --git a/src/domain/document/vendor/Camunda8Defaults.ts b/src/domain/document/vendor/Camunda8Defaults.ts
index 98af312d..2d44305c 100644
--- a/src/domain/document/vendor/Camunda8Defaults.ts
+++ b/src/domain/document/vendor/Camunda8Defaults.ts
@@ -21,7 +21,7 @@ export class Camunda8Defaults {
* which the v2 user task API does not list.
*/
userTask(el: El): boolean {
- if (this.edit.platform() !== 'camunda8' || !is(el, 'bpmn:UserTask')) return false;
+ if (this.edit.platform() !== 'c8' || !is(el, 'bpmn:UserTask')) return false;
const container = el.get('extensionElements');
const values = container ? many(container, 'values') : [];
if (values.some((extension) => extension.$type.endsWith(':userTask') && this.doc.namespaceUri(extension.$type.slice(0, extension.$type.indexOf(':'))) === ZEEBE_URI)) return false;
@@ -42,7 +42,7 @@ export class Camunda8Defaults {
* definitions without an id; the Modeler gives every definition one.
*/
definitionId(event: El, def: El): void {
- if (this.edit.platform() !== 'camunda8' || def.get('id')) return;
+ if (this.edit.platform() !== 'c8' || def.get('id')) return;
const eventId = event.get('id') ?? '';
const label = (eventId && speakingStem(eventId)) || new ElementLabels(event).label();
def.set('id', this.edit.allocator.allocate(typeRequest(def.$type, { context: label })).id);
diff --git a/src/domain/document/vendor/PlatformDetection.ts b/src/domain/document/vendor/PlatformDetection.ts
index 6e830995..c890049a 100644
--- a/src/domain/document/vendor/PlatformDetection.ts
+++ b/src/domain/document/vendor/PlatformDetection.ts
@@ -6,6 +6,15 @@ import { ZEEBE_URI } from './ZeebeDescriptor.js';
export type Platform = 'c7' | 'c8' | 'none';
export type PlatformChoice = Platform | 'auto';
+
+/** How `bpmn new --target` and `NewDocOptions.target` spell a platform; camunda7 also covers CIB seven and Operaton. */
+export const TARGETS = ['camunda8', 'camunda7', 'none'] as const;
+export type Target = (typeof TARGETS)[number];
+const PLATFORM_OF_TARGET: Readonly> = { camunda8: 'c8', camunda7: 'c7', none: 'none' };
+
+export function platformOfTarget(target: Target | undefined): Platform {
+ return target ? PLATFORM_OF_TARGET[target] : 'none';
+}
export const PLATFORM_CHOICES: readonly PlatformChoice[] = ['auto', 'c7', 'c8', 'none'];
export interface PlatformInfo {
@@ -73,10 +82,6 @@ export class PlatformDetection {
return detected.operaton ? { ...info, operaton: true, detail: `--platform option; the file uses the operaton namespace: ${OPERATON_NOTE}` } : info;
}
- target(): 'camunda7' | 'camunda8' | undefined {
- const platform = this.info().platform;
- return platform === 'c7' ? 'camunda7' : platform === 'c8' ? 'camunda8' : undefined;
- }
private fromExecutionPlatform(value: string): Platform | undefined {
const normalized = value.trim().toLowerCase();
diff --git a/src/domain/editing/DecisionLinkWriter.ts b/src/domain/editing/DecisionLinkWriter.ts
index d000cf61..7050c5d0 100644
--- a/src/domain/editing/DecisionLinkWriter.ts
+++ b/src/domain/editing/DecisionLinkWriter.ts
@@ -10,7 +10,7 @@ import { DecisionLinkReader } from '../document/DecisionLinkReader.js';
import { CAMUNDA_URI, OPERATON_URI } from '../document/vendor/CamundaDescriptor.js';
import { ZEEBE_URI } from '../document/vendor/ZeebeDescriptor.js';
-type EngineSpelling = { kind: 'c7'; uri: string; prefix: string } | { kind: 'zeebe'; prefix: string };
+type EngineSpelling = { kind: 'c7'; uri: string; prefix: string } | { kind: 'c8'; prefix: string };
/**
* Writes a business rule task's decision link in one spelling and removes the others, so a
@@ -100,11 +100,11 @@ export class DecisionLinkWriter {
dropAttr('calledDecision');
dropC7();
dropZeebe();
- } else if (platform === 'camunda7') {
+ } else if (platform === 'c7') {
const operatonOnly = !doc.prefixFor(CAMUNDA_URI) && !!doc.prefixFor(OPERATON_URI);
const prefix = operatonOnly ? this.prefixFor(OPERATON_URI, 'operaton') : this.prefixFor(CAMUNDA_URI, 'camunda');
writeC7(`${prefix}:decisionRef`);
- } else if (platform === 'camunda8') {
+ } else if (platform === 'c8') {
const call = writeZeebe(this.reader.zeebeCalls(el)[0], 'zeebe');
if (!textValue((call as unknown as Record)['resultVariable'])) {
cs.warn(
@@ -130,7 +130,7 @@ export class DecisionLinkWriter {
if (ownC7) writeC7(ownC7);
else if (ownCall) writeZeebe(ownCall, 'zeebe');
else if (follow?.kind === 'c7') writeC7(`${this.prefixFor(follow.uri, follow.prefix)}:decisionRef`);
- else if (follow?.kind === 'zeebe') writeZeebe(this.reader.zeebeCalls(el)[0], follow.prefix);
+ else if (follow?.kind === 'c8') writeZeebe(this.reader.zeebeCalls(el)[0], follow.prefix);
else {
dropC7();
dropZeebe();
@@ -163,7 +163,7 @@ export class DecisionLinkWriter {
if (!textValue((call as unknown as Record)['decisionId'])) continue;
const prefix = call.$type.split(':')[0]!;
const declared = ((call.$descriptor as { ns?: { uri?: string } }).ns?.uri ?? this.reader.uriOf(task, prefix)) === ZEEBE_URI;
- seen.set(declared ? 'zeebe' : call.$type, declared ? { kind: 'zeebe', prefix } : undefined);
+ seen.set(declared ? 'zeebe' : call.$type, declared ? { kind: 'c8', prefix } : undefined);
}
for (const key of ['calledDecision', 'calledElement']) if (textValue(task.$attrs?.[key])) seen.set(key, undefined);
if (seen.size > 1) return undefined;
diff --git a/src/domain/editing/NestedHosts.ts b/src/domain/editing/NestedHosts.ts
index 9df0e79b..8c1a6200 100644
--- a/src/domain/editing/NestedHosts.ts
+++ b/src/domain/editing/NestedHosts.ts
@@ -92,7 +92,7 @@ export class NestedHosts {
const first: Record = {
definition: `give it a trigger first: \`bpmn set ${id} trigger=\``,
loop:
- edit.platform() === 'camunda8'
+ edit.platform() === 'c8'
? `make it a multi-instance first: \`bpmn ext add ${id} loop.zeebe:loopCharacteristics inputCollection== inputElement=- \` (creates a parallel loop; \`bpmn set ${id} loop=sequential\` for one after the other)`
: `make it a loop first: \`bpmn set ${id} loop=parallel\` (or sequential, or 'loop.camunda:collection=\${items}')`,
condition: `give it a condition first: \`bpmn set ${id} 'condition=\${...}'\` (conditional events: when=)`,
diff --git a/src/domain/editing/events/EventGatewayRule.ts b/src/domain/editing/events/EventGatewayRule.ts
index 35786a5d..86f0bac1 100644
--- a/src/domain/editing/events/EventGatewayRule.ts
+++ b/src/domain/editing/events/EventGatewayRule.ts
@@ -18,7 +18,7 @@ export class EventGatewayRule {
) {}
get rule(): 'engines' | 'bpmn' {
- return this.edit.platform() === 'camunda7' ? 'engines' : 'bpmn';
+ return this.edit.platform() === 'c7' ? 'engines' : 'bpmn';
}
/** `leaving` are the flows that disappear with the change (e.g. the flow a bridge replaces). */
diff --git a/src/domain/editing/utils/decision-link-warnings.ts b/src/domain/editing/utils/decision-link-warnings.ts
index 7c8e0bf7..b1b30d68 100644
--- a/src/domain/editing/utils/decision-link-warnings.ts
+++ b/src/domain/editing/utils/decision-link-warnings.ts
@@ -11,6 +11,6 @@ export function isDecisionLinkWarning(warning: ImportWarning): boolean {
* content. In a Camunda 7 / 8 file it stays a warning because the engines refuse it.
*/
export function reportedImportWarnings(doc: Doc): ImportWarning[] {
- if (!doc.importWarnings.some(isDecisionLinkWarning) || doc.platform() !== undefined) return doc.importWarnings;
+ if (!doc.importWarnings.some(isDecisionLinkWarning) || doc.platform() !== 'none') return doc.importWarnings;
return doc.importWarnings.filter((warning) => !isDecisionLinkWarning(warning));
}
diff --git a/src/domain/validation/BpmnlintValidator.ts b/src/domain/validation/BpmnlintValidator.ts
index 387a1411..be089b73 100644
--- a/src/domain/validation/BpmnlintValidator.ts
+++ b/src/domain/validation/BpmnlintValidator.ts
@@ -88,7 +88,7 @@ export class BpmnlintValidator {
const msg = warning.message.split('\n')[0]!;
if (own.has(msg)) continue;
const id = (warning.element as { id?: string } | undefined)?.id;
- const hint = isDecisionLinkWarning(warning) && doc.platform() === undefined ? DECISION_LINK_HINT : IMPORT_HINT;
+ const hint = isDecisionLinkWarning(warning) && doc.platform() === 'none' ? DECISION_LINK_HINT : IMPORT_HINT;
out.push({ severity: 'error', code: 'import-warning', message: `Import warning: ${msg}`, ...(id ? { element: id } : {}), key: `import|${id ?? ''}|${msg}`, hint });
}
}
diff --git a/src/index.ts b/src/index.ts
index 202254db..740d03a1 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -37,7 +37,6 @@ import { RenderSvgService } from './application/service/RenderSvgService.js';
import { EditDocumentService } from './application/service/EditDocumentService.js';
import { RelayoutDiagramService } from './application/service/RelayoutDiagramService.js';
import { ReadDocumentService } from './application/service/ReadDocumentService.js';
-import { InMemoryDocumentStore } from './adapters/out/memory/InMemoryDocumentStore.js';
import { NoProjectSettings } from './adapters/out/memory/NoProjectSettings.js';
import { AroundText } from './adapters/in/report/AroundText.js';
import { ContextText } from './adapters/in/report/ContextText.js';
@@ -58,7 +57,6 @@ import type { DecisionLink } from './domain/document/DecisionLinkReader.js';
import type { ExtensionInfo } from './domain/document/ExtensionLister.js';
import type { LayoutOptions, LayoutResult } from './domain/diagram/layout/FullLayout.js';
import type { FormatEntry, FormatResult } from './domain/diagram/DiagramCommandService.js';
-const IN_MEMORY = new InMemoryDocumentStore();
const NO_PROJECT = new NoProjectSettings();
/* the in-memory API: strings in, strings / data out */
export { mutateDoc, layoutDoc, checkDoc, applyToXml, newXml, layoutXml, validateXml, viewXml, showXml, conventionsXml, metricsXml, findXml, extensionsXml, viewDoc, renderShown, type EditOptions, type EditResult, type EditReport, type LayoutXmlOptions, type ShownView, type ValidateXmlOptions, type ViewOptions } from './api.js';
@@ -92,7 +90,7 @@ export function aroundView(doc: Doc, id: string, opts: AroundOptions = {}): Arou
export function elementContext(doc: Doc, id: string): ElementContext { return new ShowModelService().elementContext({ doc, id }); }
export function implementationOf(el: El): Implementation | undefined { return new ShowModelService().implementation({ element: el }); }
/* the building blocks */
-export { Doc, TARGETS, type DocSource, type NewDocOptions } from './domain/document/Doc.js';
+export { Doc, type DocSource, type NewDocOptions } from './domain/document/Doc.js';
export { CliError, EXIT_CODES, type ErrorCategory, type Warning } from './domain/document/CliError.js';
export { KINDS, parseKind, kindOf, kindLabel } from './domain/document/utils/kinds.js';
export { FORMAT_OP_NAMES, isDiagramCommand } from './domain/commands/utils/op-names.js';
@@ -123,19 +121,19 @@ export { type LayoutDocOptions } from './application/port/in/RelayoutDiagramUseC
export type { ValidationDelta } from './domain/validation/FindingBaseline.js';
/* text-preserving output, for hosts that serialise models themselves */
export { type PreservedText } from './domain/document/xml/TextPreservation.js';
-export function preserveText(original: string, baseline: string, next: string): Promise { return new EditDocumentService(IN_MEMORY, IN_MEMORY, NO_PROJECT).preserveText({ original, baseline, next }); }
+export function preserveText(original: string, baseline: string, next: string): Promise { return new EditDocumentService(NO_PROJECT).preserveText({ original, baseline, next }); }
/* a host's validators, run inside the write transaction and by validate */
export type { NamedValidator, Validator, ValidatorContext, ValidatorFinding, ValidatorFn, ValidatorIssue, ValidatorReport, ValidatorSeverity } from './domain/validation/validators.js';
/* the project's bpmnlint configuration as an advisory validator; bpmnlint itself is injected */
export { BPMNLINT_COVERS, BPMNLINT_VALIDATOR, type BpmnlintValidatorOptions } from './domain/validation/BpmnlintValidator.js';
export type { BpmnlintConfig, BpmnlintEngine, BpmnlintLinter, BpmnlintLinterClass, BpmnlintReport, BpmnlintResolver, BpmnlintRule, BpmnlintRuleSetting, BpmnlintSeverity, BpmnlintUnavailable, ResolvedBpmnlintConfig } from './domain/validation/bpmnlint-validator.js';
/** The advisory validator of a resolved bpmnlint configuration. */
-export function bpmnlintValidator(engine: BpmnlintEngine, resolved: ResolvedBpmnlintConfig, opts: BpmnlintValidatorOptions = {}): NamedValidator { return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).bpmnlintValidator({ ...opts, engine, resolved }); }
+export function bpmnlintValidator(engine: BpmnlintEngine, resolved: ResolvedBpmnlintConfig, opts: BpmnlintValidatorOptions = {}): NamedValidator { return new ValidateDocumentService(NO_PROJECT).bpmnlintValidator({ ...opts, engine, resolved }); }
/**
* The effective bpmnlint configuration; `opts.moddleExtensions` holds the descriptors the host
* resolved for the module paths of `config.moddleExtensions`.
*/
-export async function resolveBpmnlintConfig(engine: BpmnlintEngine, config: BpmnlintConfig, opts: { moddleExtensions?: Record } = {}): Promise { return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).resolveBpmnlintConfig({ ...opts, engine, config }); }
+export async function resolveBpmnlintConfig(engine: BpmnlintEngine, config: BpmnlintConfig, opts: { moddleExtensions?: Record } = {}): Promise { return new ValidateDocumentService(NO_PROJECT).resolveBpmnlintConfig({ ...opts, engine, config }); }
/* declared id conventions: what element-id-naming in the .bpmnlintrc says about new ids */
export { renderConventions } from './domain/document/utils/conventions-text.js';
export { ID_NAMING_RULE, type IdConventions, type IdNamingHelpers, type QualifierMode } from './domain/document/ids/IdConventionsReader.js';
@@ -168,9 +166,9 @@ export function decisionLinkOf(el: El, doc?: Doc): DecisionLink | undefined {
export { ChangeSet, type Change } from './domain/document/ChangeSet.js';
export type { ValidateOptions, ValidationResult } from './domain/validation/StructuralValidator.js';
/** Structural validation and lint of a document, with the platform profile on request. */
-export function validateDoc(doc: Doc, opts: ValidateOptions = {}): ValidationResult { return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).validateStructure({ ...opts, doc }); }
+export function validateDoc(doc: Doc, opts: ValidateOptions = {}): ValidationResult { return new ValidateDocumentService(NO_PROJECT).validateStructure({ ...opts, doc }); }
/* the platform profile: Camunda 7 / 8 rules, loaded on demand */
-export { PLATFORM_CHOICES, type Platform, type PlatformChoice, type PlatformInfo } from './domain/document/vendor/PlatformDetection.js';
+export { PLATFORM_CHOICES, TARGETS, type Platform, type PlatformChoice, type PlatformInfo } from './domain/document/vendor/PlatformDetection.js';
export type { PlatformSummary, ProfileReport } from './domain/validation/profiles/PlatformProfiles.js';
export type { ProfileFinding, Severity } from './domain/document/utils/finding.js';
export type { ExtensionInfo } from './domain/document/ExtensionLister.js';
@@ -222,7 +220,7 @@ export function parseOps(input: unknown): EditCommand[] {
/** Throws E_IMPORT_LOSSY when bpmn-moddle dropped content on import, unless `force`. */
export function assertLossless(doc: Doc, opts: { force?: boolean } = {}): void {
- new ReadDocumentService(IN_MEMORY).assertLossless({ ...opts, doc });
+ new ReadDocumentService().assertLossless({ ...opts, doc });
}
/**
@@ -230,17 +228,17 @@ export function assertLossless(doc: Doc, opts: { force?: boolean } = {}): void {
* validateDoc; the async API loads them itself. A failed load (E_PROFILE_LOAD) is retried later.
*/
export function loadProfile(platform: Platform): Promise {
- return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).loadProfile({ platform });
+ return new ValidateDocumentService(NO_PROJECT).loadProfile({ platform });
}
/** Loads the engine rules of several platforms (default: all). */
export function loadProfiles(platforms?: readonly Exclude[]): Promise {
- return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).loadProfiles(platforms !== undefined ? { platforms } : {});
+ return new ValidateDocumentService(NO_PROJECT).loadProfiles(platforms !== undefined ? { platforms } : {});
}
/** Runs the profile of the document's platform (or `choice`); its rules must be loaded. */
export function runProfile(doc: Doc, choice?: PlatformChoice): ProfileReport {
- return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).runProfile({ doc, ...(choice !== undefined ? { choice } : {}) });
+ return new ValidateDocumentService(NO_PROJECT).runProfile({ doc, ...(choice !== undefined ? { choice } : {}) });
}
/**
@@ -248,7 +246,7 @@ export function runProfile(doc: Doc, choice?: PlatformChoice): ProfileReport {
* uses, else a declared one.
*/
export function detectPlatform(doc: Doc): PlatformInfo {
- return new ValidateDocumentService(IN_MEMORY, NO_PROJECT).detectPlatform({ doc });
+ return new ValidateDocumentService(NO_PROJECT).detectPlatform({ doc });
}
/** `bpmn guide`: the agent cheat sheet. */
@@ -319,5 +317,5 @@ export function runFormatOps(doc: Doc, entries: readonly FormatEntry[], asFile =
* treat the returned XML as the truth and re-parse it if you need DI.
*/
export function layoutModel(model: Model, opts: LayoutOptions = {}): Promise {
- return new RelayoutDiagramService(IN_MEMORY, IN_MEMORY, NO_PROJECT).layoutModel({ ...opts, model });
+ return new RelayoutDiagramService(NO_PROJECT).layoutModel({ ...opts, model });
}
diff --git a/src/node.ts b/src/node.ts
index 455e16ae..2d4619ef 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -37,20 +37,20 @@ export function readDoc(file: string): Promise { return new ReadDocumentSer
/** Loads a file; fails with E_IMPORT_LOSSY when bpmn-moddle dropped content, unless force. */
export function loadDoc(file: string, opts: { force?: boolean } = {}): Promise { return new ReadDocumentService(FILES).read({ source: { file }, lossless: true, ...opts }); }
/** Loads a file, applies ops and writes it back (or to opts.out). */
-export function mutateFile(file: string, ops: EditCommand[], opts: FileMutationOptions = {}): Promise { return new EditDocumentService(FILES, FILES, PROJECT).edit({ ...opts, source: { file }, commands: ops }); }
+export function mutateFile(file: string, ops: EditCommand[], opts: FileMutationOptions = {}): Promise { return new EditDocumentService(PROJECT, FILES, FILES).edit({ ...opts, source: { file }, commands: ops }); }
/** Applies ops to a loaded document and writes it to opts.out ?? doc.file. */
-export function mutateDocToFile(doc: Doc, ops: EditCommand[], opts: FileMutationOptions = {}): Promise { return new EditDocumentService(FILES, FILES, PROJECT).edit({ ...opts, source: { doc }, commands: ops }); }
+export function mutateDocToFile(doc: Doc, ops: EditCommand[], opts: FileMutationOptions = {}): Promise { return new EditDocumentService(PROJECT, FILES, FILES).edit({ ...opts, source: { doc }, commands: ops }); }
/** `bpmn layout`: redraws a file (or with `tidy` only removes overlaps) and writes it back. */
-export function layoutFile(file: string, opts: FileLayoutOptions = {}): Promise { return new RelayoutDiagramService(FILES, FILES, PROJECT).relayout({ ...opts, source: { file } }); }
+export function layoutFile(file: string, opts: FileLayoutOptions = {}): Promise { return new RelayoutDiagramService(PROJECT, FILES, FILES).relayout({ ...opts, source: { file } }); }
/** `bpmn layout` on a loaded document, written to opts.out ?? doc.file unless dryRun. */
-export function layoutDocToFile(doc: Doc, opts: FileLayoutOptions = {}): Promise { return new RelayoutDiagramService(FILES, FILES, PROJECT).relayout({ ...opts, source: { doc } }); }
+export function layoutDocToFile(doc: Doc, opts: FileLayoutOptions = {}): Promise { return new RelayoutDiagramService(PROJECT, FILES, FILES).relayout({ ...opts, source: { doc } }); }
/** `bpmn validate`: validates a file and dry-runs the layout without writing anything. */
-export function checkFile(file: string, opts: FileCheckOptions = {}): Promise { return new ValidateDocumentService(FILES, PROJECT).validate({ ...opts, source: { file } }); }
+export function checkFile(file: string, opts: FileCheckOptions = {}): Promise { return new ValidateDocumentService(PROJECT, FILES).validate({ ...opts, source: { file } }); }
/**
* checkFile for a document read elsewhere (`bpmn validate -`); without a file
* the .bpmnlintrc is looked up from `opts.lintFrom`.
*/
-export function checkLoadedDoc(doc: Doc, opts: FileCheckOptions = {}): Promise { return new ValidateDocumentService(FILES, PROJECT).validate({ ...opts, source: { doc } }); }
+export function checkLoadedDoc(doc: Doc, opts: FileCheckOptions = {}): Promise { return new ValidateDocumentService(PROJECT, FILES).validate({ ...opts, source: { doc } }); }
export { type PngImage, type PngRenderResult } from './adapters/out/files/PngRenderer.js';
export { BPMNLINT_CONFIG_FILE } from './adapters/out/files/ConfigLookup.js';
export type { BpmnlintChain, BpmnlintTier } from './adapters/out/files/ChainResolver.js';
diff --git a/test/domain/editing/c7-semantic.test.ts b/test/domain/editing/c7-semantic.test.ts
index 3bf65030..88139da3 100644
--- a/test/domain/editing/c7-semantic.test.ts
+++ b/test/domain/editing/c7-semantic.test.ts
@@ -19,7 +19,6 @@ import { renderDetail, renderView, runOps, mutateDoc, buildView, elementDetail,
import type { EditCommand } from '../../../src/domain/commands/types.js';
import { definitionsXml, flowBetween } from '../../helpers/helpers.js';
import { CAMUNDA as C7_DESCRIPTOR } from '../../../src/domain/document/vendor/CamundaDescriptor.js';
-import { PlatformDetection } from '../../../src/domain/document/vendor/PlatformDetection.js';
import { VendorDescriptor } from '../../../src/domain/document/vendor/VendorDescriptor.js';
import { ElementProperties } from '../../../src/domain/editing/ElementProperties.js';
import { caught } from '../../helpers/errors.js';
@@ -62,14 +61,14 @@ describe('C7 target: history time to live and platform version', () => {
expect(xml).toMatch(/]*camunda:historyTimeToLive="180"/);
expect(xml).toContain('modeler:executionPlatform="Camunda Platform"');
expect(xml).toContain('modeler:executionPlatformVersion="7.24.0"');
- expect(new PlatformDetection(doc.definitions).target()).toBe('camunda7');
+ expect(doc.platform()).toBe('c7');
});
it('other targets get no TTL', async () => {
expect(await Doc.create({ target: 'camunda8' }).toXml()).not.toContain('historyTimeToLive');
expect(await Doc.create({}).toXml()).not.toContain('historyTimeToLive');
- expect(new PlatformDetection(Doc.create({ target: 'camunda8' }).definitions).target()).toBe('camunda8');
- expect(new PlatformDetection(Doc.create({}).definitions).target()).toBeUndefined();
+ expect(Doc.create({ target: 'camunda8' }).platform()).toBe('c8');
+ expect(Doc.create({}).platform()).toBe('none');
});
it('a process created later in a C7 file (participant) gets the TTL too, in other files not', async () => {
@@ -93,7 +92,7 @@ describe('C7 target: history time to live and platform version', () => {
it('a loaded C7 file (modeler:executionPlatform) is recognised; an explicit TTL is never overwritten', async () => {
const xml = definitionsXml('', { nsDecl: `${CAMUNDA} xmlns:modeler="http://camunda.org/schema/modeler/1.0" modeler:executionPlatform="Camunda Platform"` });
const doc = await Doc.fromXml(xml);
- expect(new PlatformDetection(doc.definitions).target()).toBe('camunda7');
+ expect(doc.platform()).toBe('c7');
const cs = runOps(doc, [{ op: 'add', kind: 'participant', name: 'Other' }, { op: 'add', kind: 'participant', name: 'Third' }]);
const processElement = doc.require(cs.created.find((change) => change.kind === 'process')!.id);
expect(processElement.$attrs['camunda:historyTimeToLive']).toBe('180');
diff --git a/test/domain/validation/c7-profile-rules.test.ts b/test/domain/validation/c7-profile-rules.test.ts
index 4990e4dc..22e186f4 100644
--- a/test/domain/validation/c7-profile-rules.test.ts
+++ b/test/domain/validation/c7-profile-rules.test.ts
@@ -22,7 +22,7 @@ import { join } from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import { Doc } from '../../../src/domain/document/Doc.js';
import { detectPlatform, loadProfiles, runProfile } from '../../../src/index.js';
-import { PlatformDetection } from '../../../src/domain/document/vendor/PlatformDetection.js';
+import type { Platform } from '../../../src/domain/document/vendor/PlatformDetection.js';
import { validateDoc } from '../../../src/index.js';
import { cli } from '../../helpers/cli.js';
@@ -252,19 +252,18 @@ describe('profile follow-ups: one synthetic model per rule', () => {
});
describe('one platform detector', () => {
- const cases: Array<[string, string, string, 'camunda7' | 'camunda8' | undefined]> = [
- ['Camunda Platform', C7, chain([UT()]), 'camunda7'],
- ['Operaton namespace only', OPERATON, chain([UT('T')]).replace('', ''), 'camunda7'],
- ['camunda content with both namespaces declared', `${CAMUNDA} ${ZEEBE}`, chain([UT('T')]).replace('', ''), 'camunda7'],
- ['executionPlatform Operaton', `${MODELER} modeler:executionPlatform="Operaton"`, chain([UT()]), 'camunda7'],
- ['zeebe content', `${ZEEBE} ${CAMUNDA}`, chain([UT('T', ext(''))]), 'camunda8'],
- ['nothing', '', chain([UT()]), undefined],
+ const cases: Array<[string, string, string, Platform]> = [
+ ['Camunda Platform', C7, chain([UT()]), 'c7'],
+ ['Operaton namespace only', OPERATON, chain([UT('T')]).replace('', ''), 'c7'],
+ ['camunda content with both namespaces declared', `${CAMUNDA} ${ZEEBE}`, chain([UT('T')]).replace('', ''), 'c7'],
+ ['executionPlatform Operaton', `${MODELER} modeler:executionPlatform="Operaton"`, chain([UT()]), 'c7'],
+ ['zeebe content', `${ZEEBE} ${CAMUNDA}`, chain([UT('T', ext(''))]), 'c8'],
+ ['nothing', '', chain([UT()]), 'none'],
];
it.each(cases)('Doc.platform() follows detectPlatform: %s', async (_name, ns, body, expected) => {
const doc = await Doc.fromXml(xml(body, { ns, attrs: '' }));
- const platform = detectPlatform(doc).platform;
- expect(new PlatformDetection(doc.definitions).target()).toBe(expected);
- expect(doc.platform()).toBe(platform === 'c7' ? 'camunda7' : platform === 'c8' ? 'camunda8' : undefined);
+ expect(detectPlatform(doc).platform).toBe(expected);
+ expect(doc.platform()).toBe(expected);
});
it('says when the file is read as Operaton reads it, and names both declared namespaces', async () => {
diff --git a/test/helpers/helpers.ts b/test/helpers/helpers.ts
index bb1425c6..01416b36 100644
--- a/test/helpers/helpers.ts
+++ b/test/helpers/helpers.ts
@@ -18,10 +18,10 @@ const project = new BpmnlintProjectSettings();
/** The use cases on the file system, as src/cli.ts wires them. */
export const FILE_USE_CASES: CliUseCases = {
- editDocument: new EditDocumentService(files, files, project),
- createDocument: new CreateDocumentService(files, project),
- relayoutDiagram: new RelayoutDiagramService(files, files, project),
- validateDocument: new ValidateDocumentService(files, project),
+ editDocument: new EditDocumentService(project, files, files),
+ createDocument: new CreateDocumentService(project, files),
+ relayoutDiagram: new RelayoutDiagramService(project, files, files),
+ validateDocument: new ValidateDocumentService(project, files),
readDocument: new ReadDocumentService(files),
showModel: new ShowModelService(),
showConventions: new ShowConventionsService(project),