Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 32 additions & 1 deletion apps/bpmn-webview/src/app/modeler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ import ElementTemplateChooserModule from "@bpmn-io/element-template-chooser";
import TransactionBoundariesModule from "camunda-transaction-boundaries";
import { CreateAppendElementTemplatesModule } from "bpmn-js-create-append-anything";
import { BpmnModelerSetting, NoModelerError } from "@bpmn-modeler/shared";
import ImplementationLinkModule, {
ImplementationLink,
} from "@bpmn-modeler/implementation-link";
import { createReviver } from "bpmn-js-native-copy-paste/lib/PasteUtil.js";
import { ViewportData } from "./vscode";

Expand Down Expand Up @@ -50,7 +53,11 @@ export class BpmnModeler {
* @throws {UnsupportedEngineError} If the engine string is not recognised.
*/
create(engine: "c7" | "c8"): void {
const commonModules = [TokenSimulationModule, ElementTemplateChooserModule];
const commonModules = [
TokenSimulationModule,
ElementTemplateChooserModule,
ImplementationLinkModule,
];

this.engine = engine;

Expand Down Expand Up @@ -357,6 +364,30 @@ export class BpmnModeler {
});
}

/**
* Returns the `implementationLink` module instance from the modeler.
*
* @throws {NoModelerError} If the modeler has not been created yet.
*/
getImplementationLink(): ImplementationLink {
return this.getModeler().get<ImplementationLink>("implementationLink");
}

/**
* Subscribes to the `implementationLink.navigate` event fired when the user
* clicks an implementation-link overlay.
*
* @param cb Callback invoked with the BPMN activity ID to navigate to.
* @throws {NoModelerError} If the modeler has not been created yet.
*/
onImplementationLinkNavigate(cb: (activityId: string) => void): void {
this.getModeler()
.get<any>("eventBus")
.on("implementationLink.navigate", (event: any) => {
cb(event.activityId);
});
}

// ─── Private helpers ──────────────────────────────────────────────────────

/**
Expand Down
16 changes: 16 additions & 0 deletions apps/bpmn-webview/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import {
GetClipboardCommand,
GetDiagramAsSVGCommand,
GetElementTemplatesCommand,
ImplementationMapQuery,
LogErrorCommand,
LogInfoCommand,
NavigateToImplementationCommand,
NoModelerError,
Query,
SetClipboardCommand,
Expand Down Expand Up @@ -85,6 +87,11 @@ window.onload = async function () {
);
}

// Wire up implementation-link navigation: overlay click → post command to host.
bpmnModeler.onImplementationLinkNavigate((activityId) => {
vscode.postMessage(new NavigateToImplementationCommand(activityId));
});

console.debug("[DEBUG] Modeler is initialized...");

vscode.postMessage(new GetElementTemplatesCommand());
Expand Down Expand Up @@ -221,6 +228,15 @@ async function onReceiveMessage(message: MessageEvent<Query | Command>): Promise
clipboardResolver.done(message.data as ClipboardQuery);
break;
}
case queryOrCommand.type === "ImplementationMapQuery": {
try {
const query = message.data as ImplementationMapQuery;
bpmnModeler.getImplementationLink().updateEntries(query.entries);
} catch (error: any) {
vscode.postMessage(new LogErrorCommand(errorPrefix + error.message));
}
break;
}
case queryOrCommand.type === "GetDiagramAsSVGCommand": {
try {
const command = message.data as GetDiagramAsSVGCommand;
Expand Down
3 changes: 3 additions & 0 deletions apps/modeler-plugin/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -190,5 +190,8 @@
}
}
}
},
"dependencies": {
"@xmldom/xmldom": "^0.8.11"
}
}
23 changes: 19 additions & 4 deletions apps/modeler-plugin/src/controller/BpmnEditorController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,18 @@ import {
window,
} from "vscode";

import { Command, SetClipboardCommand, SyncDocumentCommand } from "@bpmn-modeler/shared";
import {
Command,
NavigateToImplementationCommand,
SetClipboardCommand,
SyncDocumentCommand,
} from "@bpmn-modeler/shared";

import { EditorStore } from "../infrastructure/EditorStore";
import { VsCodeUI } from "../infrastructure/VsCodeUI";
import { BpmnModelerService } from "../service/BpmnModelerService";
import { ArtifactService } from "../service/ArtifactService";
import { ImplementationMapService } from "../service/ImplementationMapService";

/** VS Code view-type identifier for the BPMN custom editor. */
const BPMN_VIEW_TYPE = "bpmn-modeler.bpmn";
Expand All @@ -31,12 +37,14 @@ export class BpmnEditorController implements CustomTextEditorProvider {
* @param bpmnService BPMN-specific business logic and session management.
* @param artifactSvc Workspace artifact discovery and watcher creation.
* @param vsUI User-facing message and logging helper.
* @param implMapSvc Service task → source code linking service.
*/
constructor(
private readonly editorStore: EditorStore,
private readonly bpmnService: BpmnModelerService,
private readonly artifactSvc: ArtifactService,
private readonly vsUI: VsCodeUI,
private readonly implMapSvc: ImplementationMapService,
) {}

/**
Expand Down Expand Up @@ -81,6 +89,7 @@ export class BpmnEditorController implements CustomTextEditorProvider {
this.editorStore.subscribeToTabChangeEvent(editorId);
this.editorStore.subscribeToDisposeEvent(editorId, () => {
this.bpmnService.disposeSession(editorId);
this.implMapSvc.dispose(editorId);
});

const { disposables, errors } = await this.artifactSvc.createWatcher(
Expand Down Expand Up @@ -135,10 +144,16 @@ export class BpmnEditorController implements CustomTextEditorProvider {
(message as SetClipboardCommand).text,
);
break;
case "SyncDocumentCommand":
await this.bpmnService.sync(
case "SyncDocumentCommand": {
const content = (message as SyncDocumentCommand).content;
await this.bpmnService.sync(id, content);
this.implMapSvc.update(id, content);
break;
}
case "NavigateToImplementationCommand":
this.implMapSvc.navigate(
id,
(message as SyncDocumentCommand).content,
(message as NavigateToImplementationCommand).activityId,
);
break;
}
Expand Down
77 changes: 77 additions & 0 deletions apps/modeler-plugin/src/domain/implementation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* Domain types for service task → source code linking.
*
* An {@link ImplementationEntry} describes a single implementation reference
* extracted from a BPMN service task (or send/business-rule task). The lookup
* map uses the BPMN element ID (activity ID) as key.
*/

/** Discriminator for the type of implementation reference found on a BPMN task. */
export type ImplementationKind =
| "javaClass"
| "delegateExpression"
| "expression"
| "externalTask"
| "jobType";

/**
* Value object representing a resolved (or unresolved) implementation reference.
*
* Stored as the value in the per-editor lookup map, keyed by BPMN activity ID.
*/
export interface ImplementationEntry {
/** Discriminator for the type of implementation reference. */
readonly kind: ImplementationKind;
/** Raw value from the BPMN XML, e.g. `"com.example.MyDelegate"` or `"payment-topic"`. */
readonly identifier: string;
/** Resolved absolute file path, or `undefined` if unresolved. */
readonly filePath?: string;
/** Display text for the webview overlay, e.g. `"MyDelegate"` or `"payment-topic"`. */
readonly label: string;
/** `true` when {@link filePath} points to an existing file. */
readonly resolved: boolean;
}

/**
* Intermediate extraction result produced by the BPMN XML parser before
* file resolution takes place.
*/
export interface RawImplementationRef {
/** BPMN element `id` attribute. */
readonly activityId: string;
/** Type of implementation reference. */
readonly kind: ImplementationKind;
/** Raw identifier value from the XML. */
readonly identifier: string;
}

/**
* A single I/O parameter extracted from the BPMN XML extension elements.
*/
export interface RawIOParameter {
/** Parameter name. */
readonly name: string;
/** Whether this is an input or output parameter. */
readonly direction: "input" | "output";
/** Raw expression / value from the XML. */
readonly value?: string;
}

/**
* Full extraction result for a single activity, including implementation
* reference and I/O parameters.
*
* Produced by {@link extractActivityDetails} in the XML parser.
*/
export interface RawActivityExtraction {
/** BPMN element `id` attribute. */
readonly activityId: string;
/** BPMN element `name` attribute. */
readonly activityName: string;
/** Implementation reference, if present. */
readonly implementation?: RawImplementationRef;
/** Input parameters extracted from the XML. */
readonly inputs: RawIOParameter[];
/** Output parameters extracted from the XML. */
readonly outputs: RawIOParameter[];
}
130 changes: 130 additions & 0 deletions apps/modeler-plugin/src/domain/persistedMap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* Pure domain types and builder for the persisted implementation map.
*
* The persisted map is a JSON file that captures the process-to-code mapping
* for a single BPMN file. It lives under
* `<configFolder>/implementation-map/<bpmnFileName>.json` and uses
* workspace-relative paths so the file is portable and committable.
*/
import { ImplementationKind } from "./implementation";

/**
* Root structure of the persisted JSON file.
*
* One file is created per BPMN file, containing all resolved (and unresolved)
* implementation references along with I/O parameter metadata.
*/
export interface PersistedProcessMap {
/** JSON schema URL for validation. */
readonly $schema: string;
/** Schema version — allows future migrations. */
readonly version: 1;
/** BPMN process ID extracted from `<bpmn:process id="...">`. */
readonly processId: string;
/** Camunda engine variant the BPMN was authored for. */
readonly engine: "c7" | "c8";
/** ISO 8601 timestamp of last map generation. */
readonly lastUpdated: string;
/** Activity entries keyed by BPMN element ID. */
readonly activities: Record<string, PersistedActivityEntry>;
}

/**
* A single activity (service/send/business-rule task) in the persisted map.
*/
export interface PersistedActivityEntry {
/** BPMN element `name` attribute. */
readonly name: string;
/** Implementation reference details. */
readonly implementation: PersistedImplementation;
/** Input parameters extracted from the BPMN XML. */
readonly inputs: PersistedVariable[];
/** Output parameters extracted from the BPMN XML. */
readonly outputs: PersistedVariable[];
}

/**
* Persisted representation of an implementation reference.
*/
export interface PersistedImplementation {
/** Discriminator for the reference type. */
readonly kind: ImplementationKind;
/** Raw identifier value from the XML. */
readonly identifier: string;
/** Workspace-relative file path, or `null` if unresolved. */
readonly filePath: string | null;
/** Whether the file path points to an existing file. */
readonly resolved: boolean;
}

/**
* An I/O variable extracted from the BPMN XML.
*/
export interface PersistedVariable {
/** Parameter name. */
readonly name: string;
/** Raw expression / value from the XML. */
readonly value?: string;
}

/**
* Input data required by {@link buildPersistedMap} to assemble a persisted map.
*/
export interface BuildPersistedMapInput {
/** BPMN process ID. */
readonly processId: string;
/** Detected engine variant. */
readonly engine: "c7" | "c8";
/** Activity details keyed by BPMN element ID. */
readonly activities: Record<
string,
{
readonly name: string;
readonly kind: ImplementationKind;
readonly identifier: string;
readonly filePath: string | null;
readonly resolved: boolean;
readonly inputs: PersistedVariable[];
readonly outputs: PersistedVariable[];
}
>;
}

/** Placeholder schema URL — can be replaced with a real URL once published. */
const SCHEMA_URL =
"https://raw.githubusercontent.com/Miragon/bpmn-vscode-modeler/main/schemas/implementation-map.v1.json";

/**
* Assembles a {@link PersistedProcessMap} from in-memory data.
*
* Pure function with no side effects — fully testable.
*
* @param input Aggregated data from the XML parser and implementation map.
* @returns A complete persisted map ready for JSON serialisation.
*/
export function buildPersistedMap(input: BuildPersistedMapInput): PersistedProcessMap {
const activities: Record<string, PersistedActivityEntry> = {};

for (const [activityId, data] of Object.entries(input.activities)) {
activities[activityId] = {
name: data.name,
implementation: {
kind: data.kind,
identifier: data.identifier,
filePath: data.filePath,
resolved: data.resolved,
},
inputs: data.inputs,
outputs: data.outputs,
};
}

return {
$schema: SCHEMA_URL,
version: 1,
processId: input.processId,
engine: input.engine,
lastUpdated: new Date().toISOString(),
activities,
};
}
Loading
Loading