Skip to content
Open
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
15 changes: 13 additions & 2 deletions packages/core/src/browser/new-element-contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import {
RelationshipType,
TargetObjectType,
findNextUnique,
getCrossModelEdition,
getCrossModelVersion,
isMemberPermittedInModel,
quote,
toId,
Expand Down Expand Up @@ -57,12 +59,21 @@ interface NewElementTemplate<T extends readonly InputOptions[] = readonly InputO
getInputOptions?(parent: URI, modelService: ModelService): MaybePromise<T>;
}

const INITIAL_DATAMODEL_CONTENT = `datamodel:
/**
* Generates the initial datamodel content with current CrossModel version.
* The version is read from CROSSMODEL_VERSION constant which is sourced from package.json.
*/
function getInitialDataModelContent(): string {
return `datamodel:
id: _
name: ""
type: ${DataModelTypeInfos.logical.value}
version: 1.0.0
crossmodel:
edition: ${getCrossModelEdition()}
version: ${getCrossModelVersion()}
`;
}

const INITIAL_ENTITY_CONTENT = `entity:
id: _
Expand Down Expand Up @@ -152,7 +163,7 @@ const NEW_ELEMENT_TEMPLATES: ReadonlyArray<NewElementTemplate> = [
validateName: validateDataModelName,
iconClass: ModelStructure.DataModel.ICON_CLASS,
toUri: (parent, name) => parent.resolve(toId(name)).resolve(DATAMODEL_FILE),
content: INITIAL_DATAMODEL_CONTENT
content: getInitialDataModelContent
}
];

Expand Down
1 change: 1 addition & 0 deletions packages/protocol/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,4 @@ export * from './integration';
export * from './model';
export * from './model-service/protocol';
export * from './util';
export * from './version';
1 change: 1 addition & 0 deletions packages/protocol/src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export const RESERVED_KEYWORDS = [
'childRole',
'conceptual',
'conditions',
'crossmodel',
'cross-join',
'customProperties',
'datamodel',
Expand Down
65 changes: 65 additions & 0 deletions packages/protocol/src/version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/********************************************************************************
* Copyright (c) 2026 CrossBreeze.
********************************************************************************/

/**
* Current CrossModel edition and version information.
* These are automatically written to datamodel.cm files to enable detection of breaking changes
* when models are opened in different CrossModel versions.
*
* The version and edition are sourced from the package.json file,
* which is kept in sync with lerna.json by the monorepo build system.
*/

// Cache for version and edition to avoid repeated file reads
let cachedVersion: string | undefined;
let cachedEdition: string | undefined;
const CROSSMODEL_EDITION_PREFIX = 'crossmodel-';

function getPackageJson(): { name?: string; version?: string } | undefined {
try {
// Try to read using require (Node.js environment)
// eslint-disable-next-line @typescript-eslint/no-var-requires, global-require, import/no-dynamic-require
const pkg = require('../package.json');
if (pkg?.version) {
return pkg;
}
return pkg;
} catch {
// If require fails (browser environment), version will be set to 0.0.0
return undefined;
}
}

function populatePackageMetadata(): void {
if (cachedVersion !== undefined && cachedEdition !== undefined) {
// Already cached, no need to read again
return;
}
const pkg = getPackageJson();
cachedVersion = pkg?.version ?? '0.0.0';
if (pkg?.name?.startsWith(CROSSMODEL_EDITION_PREFIX)) {
// Extract edition from package name (e.g., 'crossmodel-core' -> 'core')
cachedEdition = pkg.name.substring(CROSSMODEL_EDITION_PREFIX.length);
} else {
cachedEdition = 'core'; // fallback
}
}

function getVersionFromPackage(): string {
populatePackageMetadata();
return cachedVersion!;
}

function getEditionFromPackage(): string {
populatePackageMetadata();
return cachedEdition!;
}

export function getCrossModelVersion(): string {
return getVersionFromPackage();
}

export function getCrossModelEdition(): string {
return getEditionFromPackage();
}
Original file line number Diff line number Diff line change
Expand Up @@ -238,12 +238,14 @@ export class CrossModelDataModelManager {
`[DataModel] Re-build affected documents (${docs.length}): ${docs.map(doc => workspace.wsRelativePath(doc.uri)).join(', ')}`
);
docs.forEach(doc => {
this.langiumDocuments.invalidateDocument(doc.uri);
// Force re-parse/re-build of the document
this.documentBuilder.resetToState(doc, DocumentState.Changed);
changed.push(doc.uri);
});
}
}

// Adds the data model for the given URI. Returns the ID of the added data model.
protected addDataModel(uri: URI, dataModel: DataModel): string[] {
const dataModelInfo = new DataModelInfo(uri, dataModel);
if (!dataModelInfo.isUnknown) {
Expand Down Expand Up @@ -285,6 +287,7 @@ export class CrossModelDataModelManager {
return [];
}

// Updates (or adds) the data model for the given URI. Returns the IDs of data models that were added/updated.
protected async updateDataModel(uri: URI): Promise<string[]> {
const newDataModel = await parseDataModelFile(uri, this.langiumDocuments);
if (!newDataModel) {
Expand All @@ -294,10 +297,12 @@ export class CrossModelDataModelManager {
const toUpdate = [];
const existingDataModelInfo = this.uriToDataModel.get(uri.toString());
const newDataModelId = createDataModelId(newDataModel.id, newDataModel.version);
// data model ID changed, remove old one
if (existingDataModelInfo && existingDataModelInfo?.id !== newDataModelId) {
this.logger.info(`[DataModel] Replace data model "${existingDataModelInfo.id}" with "${newDataModelId}"`);
toUpdate.push(...this.deleteDataModel(uri));
}
// add new/updated data model
toUpdate.push(...this.addDataModel(uri, newDataModel));
return toUpdate;
}
Expand Down
37 changes: 37 additions & 0 deletions packages/server/src/language-server/generated/ast.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,13 +45,15 @@ export type CrossModelKeywordNames =
| "conceptual"
| "conditions"
| "cross-join"
| "crossmodel"
| "customProperties"
| "datamodel"
| "datatype"
| "dependencies"
| "description"
| "diagram"
| "edges"
| "edition"
| "entity"
| "expression"
| "expressions"
Expand Down Expand Up @@ -197,6 +199,23 @@ export function isCardinality(item: unknown): item is Cardinality {
return item === '0..1' || item === '1..1' || item === '0..N' || item === '1..N';
}

export interface CrossModelEditionInfo extends langium.AstNode {
readonly $container: DataModel;
readonly $type: 'CrossModelEditionInfo';
edition: string;
version: string;
}

export const CrossModelEditionInfo = {
$type: 'CrossModelEditionInfo',
edition: 'edition',
version: 'version'
} as const;

export function isCrossModelEditionInfo(item: unknown): item is CrossModelEditionInfo {
return reflection.isInstance(item, CrossModelEditionInfo.$type);
}

export interface CrossModelRoot extends langium.AstNode {
readonly $type: 'CrossModelRoot';
datamodel?: DataModel;
Expand Down Expand Up @@ -316,13 +335,15 @@ export function isDataElementMapping(item: unknown): item is DataElementMapping
export interface DataModel extends NamedObject, WithCustomProperties {
readonly $container: CrossModelRoot;
readonly $type: 'DataModel';
crossmodel?: CrossModelEditionInfo;
dependencies: Array<DataModelDependency>;
type: string;
version?: string;
}

export const DataModel = {
$type: 'DataModel',
crossmodel: 'crossmodel',
customProperties: 'customProperties',
dependencies: 'dependencies',
description: 'description',
Expand Down Expand Up @@ -854,6 +875,7 @@ export type CrossModelAstType = {
AttributeMappingTarget: AttributeMappingTarget
BinaryExpression: BinaryExpression
BooleanExpression: BooleanExpression
CrossModelEditionInfo: CrossModelEditionInfo
CrossModelRoot: CrossModelRoot
CustomProperty: CustomProperty
DataElement: DataElement
Expand Down Expand Up @@ -967,6 +989,18 @@ export class CrossModelAstReflection extends langium.AbstractAstReflection {
},
superTypes: []
},
CrossModelEditionInfo: {
name: CrossModelEditionInfo.$type,
properties: {
edition: {
name: CrossModelEditionInfo.edition
},
version: {
name: CrossModelEditionInfo.version
}
},
superTypes: []
},
CrossModelRoot: {
name: CrossModelRoot.$type,
properties: {
Expand Down Expand Up @@ -1075,6 +1109,9 @@ export class CrossModelAstReflection extends langium.AbstractAstReflection {
DataModel: {
name: DataModel.$type,
properties: {
crossmodel: {
name: DataModel.crossmodel
},
customProperties: {
name: DataModel.customProperties,
defaultValue: []
Expand Down
Loading
Loading