Skip to content
Merged
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
55 changes: 52 additions & 3 deletions examples/order-flow/server/test/project-visibility.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,19 @@
* holds a single file.
*/

import { AstUtils, DocumentState, type ReferenceInfo } from '@hydranium/langium';
import { describe, expect, it } from 'vitest';
import { AstUtils, DocumentState, type ReferenceInfo, URI } from '@hydranium/langium';
import { readFileSync } from 'node:fs';
import { describe, expect, it, onTestFinished } from 'vitest';
import { type DomainModel, type Entity, type Field, isEntity } from '../src/language-server/ast.js';
import { ORDER_FLOW_STDLIB_URI } from '../src/language-server/order-flow-stdlib.js';
import { documentFor, makeWorkspaceHarness, type OrderFlowHarness, workspaceUri } from './order-flow-harness.js';
import {
documentFor,
makeScratchWorkspaceHarness,
makeWorkspaceHarness,
type OrderFlowHarness,
WORKSPACE_FILES,
workspaceUri
} from './order-flow-harness.js';

function entityNamed(model: DomainModel, name: string): Entity {
const found = model.declarations.find(declaration => isEntity(declaration) && declaration.name === name);
Expand Down Expand Up @@ -211,3 +219,44 @@ describe('order-flow visibility — public declarations cross the project bounda
expect(candidates).not.toContain('Order');
});
});

describe('order-flow projects — a descriptor change', () => {
it('takes effect in the build the edit starts', async () => {
const { harness, workspace } = await makeScratchWorkspaceHarness();
onTestFinished(() => workspace.dispose());
const projects = harness.shared.workspace.ProjectManager;
const builder = harness.shared.workspace.DocumentBuilder;
const ordersPath = workspace.resolve(WORKSPACE_FILES.ordersDomain);
const ordersUri = URI.file(ordersPath);
const original = readFileSync(ordersPath, 'utf8');
const totalTypeName = (): string | undefined => {
const model = harness.shared.workspace.LangiumDocuments.getDocument(ordersUri)!.parseResult.value as DomainModel;
return fieldNamed(entityNamed(model, 'Order'), 'total').type.declared?.ref?.name;
};

workspace.write(WORKSPACE_FILES.ordersDomain, original.replace('project orders requires commerce-core', 'project orders'));
await builder.update([ordersUri], []);
expect(projects.getProjectById('orders')?.dependencies).toBeUndefined();
expect(totalTypeName()).toBeUndefined();

workspace.write(WORKSPACE_FILES.ordersDomain, original);
await builder.update([ordersUri], []);
expect(projects.getProjectById('orders')?.dependencies).toEqual(['commerce-core']);
expect(totalTypeName()).toBe('Money');
});

it('registers a descriptor created by an update in that update', async () => {
const { harness, workspace } = await makeScratchWorkspaceHarness();
onTestFinished(() => workspace.dispose());
const projects = harness.shared.workspace.ProjectManager;
const refundsUri = URI.file(
workspace.write('refunds/refunds.domain', 'project refunds requires commerce-core\n\nentity Refund {\n amount: Money\n}\n')
);

await harness.shared.workspace.DocumentBuilder.update([refundsUri], []);

expect(projects.getProjectById('refunds')?.dependencies).toEqual(['commerce-core']);
const model = harness.shared.workspace.LangiumDocuments.getDocument(refundsUri)!.parseResult.value as DomainModel;
expect(fieldNamed(entityNamed(model, 'Refund'), 'amount').type.declared?.ref?.name).toBe('Money');
});
});
10 changes: 9 additions & 1 deletion packages/core/src/langium/project/abstract-project-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,14 @@ import {
type AstNode,
AstUtils,
type DocumentBuilder,
DocumentState,
type FileSystemProvider,
type LangiumDocument,
type LangiumDocuments,
type URI,
UriUtils
} from '@hydranium/langium';
import { Disposable } from 'vscode-languageserver';
import { CancellationToken, Disposable } from 'vscode-languageserver';
import type { WorkspaceFolder } from 'vscode-languageserver-types';
import type { LogNameOptions } from '../diagnostics/logger.js';
import { type ServerSharedServicesMinimal } from '../shared-services.js';
Expand Down Expand Up @@ -377,6 +378,13 @@ export abstract class AbstractProjectManager<TProject extends Project = Project>
protected async loadDescriptor(uri: URI): Promise<TProject | undefined> {
try {
const document = await this.langiumDocuments.getOrCreateDocument(uri);
if (document.state < DocumentState.Parsed) {
// `onUpdate` fires before the build parses, so the AST is still the previous edit's.
// The state is restored so the build still runs, and notifies, its own parse phase.
const state = document.state;
await this.services.workspace.LangiumDocumentFactory.update(document, CancellationToken.None);
document.state = state;
}
const project = await this.parseProjectDescriptor(uri, document);
if (project) {
this.projects.set(project.id, project);
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/langium/project/project-change-event.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import type { URI } from '@hydranium/langium';
* - {@link affectedDocuments} lists URIs whose reachable scope changed. The
* default cascade plumbing in `HydraniumWorkspaceManager` subscribes to
* this event and re-runs `DocumentBuilder.resetToState` for each entry,
* which the next build cycle picks up. Consumers can subscribe
* independently for other reactions.
* which the build of the update that changed the descriptor picks up.
* Consumers can subscribe independently for other reactions.
*/
export interface ProjectChangeEvent<TProject extends Project = Project> {
/** Ids of projects newly registered since the last event. */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,8 +137,8 @@ export interface HydraniumWorkspaceManagerOptions extends LogNameOptions {
* URIs (its own members plus any transitive dependents per the
* `getAffectedProjects` hook). This manager re-runs
* `DocumentBuilder.resetToState(doc, DocumentState.Changed)` on each
* affected document so the next build cycle re-links them with the new
* visibility scope.
* affected document so the build of the update that changed the descriptor
* re-links them with the new visibility scope.
*
* Descriptor URIs that triggered the cycle are intentionally excluded
* from the affected set by `AbstractProjectManager.onBuildUpdate` —
Expand Down Expand Up @@ -501,9 +501,9 @@ export class HydraniumWorkspaceManager extends DefaultWorkspaceManager {

/**
* Default cascade-rebuild handler. Iterates {@link ProjectChangeEvent.affectedDocuments}
* and resets each to {@link DocumentState.Changed} so the next build cycle
* re-runs the pipeline (parse → link → validate) under the new project
* visibility scope.
* and resets each to {@link DocumentState.Changed} so the build of the update
* that changed the descriptor re-runs the pipeline (parse → link → validate)
* under the new project visibility scope.
*
* Subclasses can override for additional behaviour (e.g. logging,
* telemetry, custom rebuild policies); call `super` to preserve the
Expand Down
Loading