TSArchi is a TypeScript-based utility for parsing .archimate files and manipulating the contained ArchiMate models. The project enables reading, editing, and saving enterprise architecture models compliant with the ArchiMate standard.
TSArchi provides a TypeScript-based tool for parsing, modifying, and saving .archimate files. ArchiMate is an open, independent modeling language for enterprise architecture, and TSArchi allows you to work with these models programmatically.
- Parsing: Reads
.archimatefiles and converts them into a TypeScript object model. - Model Manipulation: Add, modify, and remove elements and relationships in the parsed model.
- Model Serialization: Save the modified model back into an
.archimatefile. - Type Safety: Enforces strong TypeScript types for all operations on the model.
- Clear Errors: Files that are not well-formed XML or not Archi models throw an
ArchimateParseError, and models are validated before they are saved. - Element Upsert: Smart insert/update operations that preserve existing IDs while updating properties.
- Comprehensive Element Support: Full support for all ArchiMate 3.x element types and relationships.
- Advanced View Management: Create, update, and manage ArchiMate diagrams with visual positioning and styling.
- Auto-layout Capabilities: Generate views automatically with grid, circular, or hierarchical layouts.
TSArchi requires Node.js 20 or later. Install it from npm:
npm install tsarchiThe tsarchi entry point holds the model API and runs in Node.js and in the browser. TsArchi, which reads and writes files, is imported from tsarchi/node.
Version 3.0.0 has breaking changes in how views are read and in the element type lists:
getView(),listViews(),createView(),addDiagramObject(),addGroup(),addDiagramObjectToGroup()andaddConnection()return copies in one shape, for views loaded from a file and views built with the API alike. Nested objects are inchildren, connections insourceConnections, and the ids of the connections ending on an object intargetConnections(an array). All three are always present, empty when there is nothing in them. Change a view through the view methods: changing a returned view no longer changes the model.ViewChildandViewConnectionhave every field ofChildandSourceConnection(figure, features, documentation, content, …), andViewChild.typeis a string, as loaded views also contain types such asDiagramModelReferenceandSketchModelSticky.- The stored form, with
child,sourceConnectionand space-separatedtargetConnectionsas in the file, is still available throughgetElement(viewId).child. MeaningandValueare motivation elements, as in Archi: they move fromarchimateBusinessElementTypes/ArchimateBusinessElementTypetoarchimateMotivationElementTypes/ArchimateMotivationElementType.
Version 2.0.0 has breaking changes. The main ones:
TsArchiis imported fromtsarchi/nodeinstead oftsarchi.Parser,Serializer,BoundsMapper,SourceConnectionMapperandDiagramAttributeMapperare no longer exported. Load and save models throughArchimateandTsArchi.TsArchi.load()andloadModel()throw anArchimateParseErrorfor a file that is not an Archi model, and pass on file read errors, instead of logging them and returning an empty result.- Every
ValidationIssuehas aseverity. Only errors stop a model from being saved; unnamed elements, relationships that Archi's relationships matrix rejects, and element or relationship types tsarchi does not know are warnings. upsertElement()matches an existing element onidwhen one is given, instead of onnameandtype.- The type lists follow Archi: types Archi never writes, such as
UsedByRelationshipandStage, are removed, and short names such asFloware no longer model types, thoughupsertRelationship()still accepts them.Junctionis an element type in the Other folder. View.backgroundis a number.Child.sourceConnectionand the schema'sModel.foldercan be a single object or an array.
See the changelog for the full list.
You can use TSArchi programmatically in your TypeScript/JavaScript projects:
import { TsArchi } from "tsarchi/node";
const tsArchi = new TsArchi();
// Load and parse an ArchiMate file
const model = await tsArchi.loadModel("./path/to/model.archimate");
// Add a new element; upsertElement returns the element as stored, with its id
const element = model.upsertElement({
type: "ApplicationComponent",
name: "My New Component",
properties: new Map([["version", "2.0"]]),
});
// Read the model's name and contents
console.log(
`${model.getName()} — ${model.listElements().length} elements, ${model.listRelationships().length} relationships`,
);
// Save the modified model
await tsArchi.saveModel("./path/to/output.archimate");Archimate.fromXml and toXml load and save the text of an .archimate file without touching the file system, for text from a browser file input, IPC or a test. TsArchi uses the same XML settings, so both give the same result:
import { Archimate, ArchimateParseError } from "tsarchi";
try {
const model = Archimate.fromXml(text);
// ...
const xml = model.toXml(); // throws ArchimateValidationError if the model has errors
} catch (error) {
if (error instanceof ArchimateParseError) {
// error.kind: "not-xml" | "not-archimate" | "invalid-structure"
// error.line / error.column: where the XML is not well-formed
console.error(error.message);
}
}TsArchi.load and loadModel throw the same ArchimateParseError, and pass on file read errors.
The tsarchi entry point uses no Node built-ins, so it can be bundled for the browser. TsArchi reads and writes files with fs/promises and is exported from tsarchi/node instead. In a browser, load and save with Archimate.fromXml and toXml.
User-created folders are kept on load and written back in place on save. Element lookups such as findElementsByFolder include elements from nested folders; getFolders returns the folder tree, where each folder lists the ids of the elements placed directly in it:
for (const folder of model.getFolders("application")) {
console.log(folder.name, folder.elementIds, folder.folders);
}New elements are added at the top level of their folder.
getFolder returns the id, name, documentation, properties and features of a top-level folder, and updateFolder changes them. A detail set to undefined, "" or an empty map is removed:
const { name, documentation } = model.getFolder("business");
model.updateFolder("business", {
documentation: "Business layer",
properties: new Map([["Owner", "EA"]]),
});getFolderById and updateFolderById do the same for any folder, nested at any depth or top-level, by its id. getFolderById returns null when there is no folder with that id, and updateFolderById throws:
model.updateFolderById("id-folder-portals", { name: "Customer Portals", documentation: undefined });createFolder adds a folder inside any folder, top-level or nested, with a generated id unless one is given. moveFolder moves a nested folder, with its content, to another folder of the same top-level folder, and moveElementToFolder moves an element, relationship or view into a folder of its own top-level folder; as in Archi, neither crosses top-level folders. deleteFolder deletes a nested folder with everything in it, including the relationships and diagram objects that deleteElement removes along with its elements:
const { id } = model.createFolder(model.getFolder("application").id, { name: "Portals" });
model.moveElementToFolder("id-portal", id);
model.moveFolder(id, "id-folder-customer");
model.deleteFolder(id);Relationships can be created and queried directly:
const relationship = model.upsertRelationship({
id: model.generateUniqueId(),
name: "App Flow",
type: "FlowRelationship",
source: "source-element-id",
target: "target-element-id",
});
const outgoing = model.findRelationshipsForElement("source-element-id", "source");
const between = model.findRelationshipsBetween("source-element-id", "target-element-id");
model.deleteRelationship(relationship.id);Access relationships keep Archi's accessType (0 write, 1 read, 2 unspecified, 3 read/write):
model.upsertRelationship({
name: "reads",
type: "AccessRelationship",
source: "process-id",
target: "object-id",
accessType: 1,
});updateElement changes an element, relationship or view by id. Keys left out are unchanged, and a key set to undefined removes that field (a removed name becomes empty, which is saved without a name attribute, as Archi does). properties is merged into the existing properties:
model.updateElement("relationship-id", { name: undefined, documentation: undefined });The model's name is available through model.getName() and model.setName(name), and its id and file format version through model.getId() and model.getVersion(). The model's purpose text is available through model.getPurpose() and model.setPurpose(text).
Model-level properties, metadata and specializations (profiles) are available the same way:
model.setProperties(new Map([["Owner", "Enterprise Architecture"]]));
model.setMetadata(new Map([["creator", "tsarchi"]]));
model.setProfiles([{ id: "profile-id", name: "Premium", conceptType: "BusinessActor" }]);
model.updateElement("element-id", { profiles: "profile-id" });Folders, elements, relationships and views keep their <feature> entries as a features map, in file order:
model.updateElement("element-id", { features: new Map([["myFeature", "value"]]) });Anything else that TSArchi does not recognise, on the model, its folders, elements, relationships, views, diagram objects and view connections, is kept in an unrecognized field and written back unchanged, so content from newer Archi versions survives a round trip.
TSArchi supports all standard ArchiMate element types organized by layers:
- Strategy Layer: Capability, CourseOfAction, Resource, ValueStream, etc.
- Business Layer: BusinessActor, BusinessRole, BusinessProcess, BusinessService, etc.
- Application Layer: ApplicationComponent, ApplicationService, DataObject, etc.
- Technology Layer: Node, Device, SystemSoftware, TechnologyService, etc.
- Motivation Layer: Stakeholder, Driver, Goal, Requirement, etc.
- Implementation & Migration: WorkPackage, Deliverable, ImplementationEvent, etc.
TypeScript consumers can import the supported type unions and runtime guard:
import type { ArchimateElementType, ArchimateRelationshipType } from "tsarchi";
import { isArchimateModelType } from "tsarchi";
const elementType: ArchimateElementType = "ApplicationComponent";
const relationshipType: ArchimateRelationshipType = "FlowRelationship";
if (isArchimateModelType(elementType)) {
// safe to use with typed model APIs
}TSArchi provides comprehensive view management capabilities for creating and manipulating ArchiMate diagrams:
import { TsArchi } from "tsarchi/node";
const tsArchi = new TsArchi();
const model = await tsArchi.loadModel("./model.archimate");
// Create a new view
const view = model.createView("Application Overview", {
viewpoint: "application",
documentation: "Overview of application components",
});
// Add elements to the view with positioning
const bounds1 = { x: 100, y: 100, width: 120, height: 55 };
const bounds2 = { x: 300, y: 100, width: 120, height: 55 };
const obj1 = model.addDiagramObject(view.id, "app-component-1-id", bounds1, {
fillColor: "#c9e7b7",
textAlignment: 1,
});
const obj2 = model.addDiagramObject(view.id, "app-component-2-id", bounds2, {
fillColor: "#ffd93d",
});
// Create connections between elements
model.addConnection(view.id, obj1.id, obj2.id, "relationship-id", {
lineColor: "#0066cc",
lineWidth: 2,
});
// Create groups to organize elements
const groupBounds = { x: 50, y: 50, width: 400, height: 150 };
const group = model.addGroup(view.id, "Application Layer", groupBounds, {
fillColor: "#e6f3ff",
documentation: "Application layer components",
});
// Add elements to groups
model.addDiagramObjectToGroup(view.id, group.id, "another-element-id", {
x: 20,
y: 20,
width: 120,
height: 55,
});Create views automatically from existing model elements:
// Generate view from specific elements
const elementIds = ["comp-1", "comp-2", "comp-3"];
const generatedView = model.generateViewFromElements("Generated View", elementIds, {
layoutType: "grid",
includeRelationships: true,
viewpoint: "application",
});
// Create view from all elements of a specific type
const appView = model.createViewByElementType("Application Components", "ApplicationComponent", {
layoutType: "circular",
includeRelationships: true,
});
// Create view from all elements in a folder
const businessView = model.createViewByFolder("Business Overview", "business", {
layoutType: "hierarchical",
});// List all views (ArchiMate, sketch and canvas)
const allViews = model.listViews();
console.log(`Found ${allViews.length} views`);
// List only ArchiMate views
const archimateViews = model.listViews({ type: "ArchimateDiagramModel" });
// Get specific view. Nested objects are in `children`, connections in `sourceConnections`
// and connection ids ending on an object in `targetConnections`. The view is a copy:
// change it through the methods below.
const view = model.getView("view-id");
for (const child of view?.children ?? []) {
console.log(child.type, child.id, child.children.length, child.sourceConnections.length);
}
// Update diagram object styling
model.updateDiagramObjectStyle("view-id", "object-id", {
fillColor: "#ff6b6b",
bounds: { x: 150, y: 150, width: 140, height: 65 },
textAlignment: 2,
});
// Delete a view, and the references to it in other views
model.deleteView("view-id");TSArchi reports problems instead of silently changing the model:
- Loading text that is not well-formed XML or not an Archi model throws an
ArchimateParseError(see Loading and Saving Text) - Missing or malformed bounds data defaults to zero values
- Duplicate elements are handled gracefully with upsert operations
- View operations validate element and relationship existence
- Models are validated before saving to catch duplicate IDs, unknown types, broken relationships, broken view references, and references to missing profiles
- Relationship types are checked against Archi's relationships matrix, including its Junction rules.
upsertRelationship()throws for a type Archi would not allow
Every issue from validateModel() has a severity. Only error issues stop a model from being saved. warning issues cover models Archi still opens and saves: unnamed elements, and relationships that Archi's matrix rejects (older models often have them). Elements and relationships with a type tsarchi does not know (unknown-type), such as content from a newer Archi or a plugin, are also warnings: they are kept as loaded and written back unchanged. upsertElement() and upsertRelationship() still throw for an unknown type.
const issues = model.validateModel();
for (const issue of issues) {
console.log(`${issue.severity}: ${issue.message}`);
}Contributions are welcome. See CONTRIBUTING.md for setting up the project, running the tests and examples, and submitting changes.
This project is licensed under the MIT License. See the LICENSE file for details.