Skip to content

feat: support self-identifying workspace schemas#39

Open
Suyog241005 wants to merge 4 commits into
hyperjump-io:mainfrom
Suyog241005:feat/self-identifying-schemas
Open

feat: support self-identifying workspace schemas#39
Suyog241005 wants to merge 4 commits into
hyperjump-io:mainfrom
Suyog241005:feat/self-identifying-schemas

Conversation

@Suyog241005

Copy link
Copy Markdown
Contributor

This PR introduces automatic discovery and lifecycle management of local workspace schemas. It detects JSON files with $schema and $id defined at the root and keeps the schema registry in sync as files are added, updated, or removed.

Key Changes

  • SelfIdentifyingSchemas: Added automatic workspace scanning on startup, dynamic schema registration by $id using @hyperjump/json-schema, and file watching to keep the registry in sync. Also added three new integration tests.

  • SchemaStore.ts: Added tracking for workspace schema $ids. Introduced unregisterSchema in clear() to prevent @hyperjump/json-schema from serving stale schemas from its global cache after files are modified or removed.

  • JsonDocument.ts: Updated dependsOn() to recognize dependencies between JSON documents and workspace schema $ids. Also added URI normalization (normalizeIri) to avoid OS-specific path mismatches.

  • build-server.ts: Initialized the workspace schema manager before Diagnostics to ensure schemas are registered before the initial validation pass.

  • TestClient.ts: Fixed path resolution in deleteDocument (resolving a TypeError) and added watcher notifications for deleted files.

  • SchemaValidation.test.ts: Improved test reliability by adding short delays to eliminate race conditions caused by duplicate diagnostic notification queues.

@Suyog241005 Suyog241005 force-pushed the feat/self-identifying-schemas branch 2 times, most recently from 5de1b92 to 5e66f7a Compare July 2, 2026 14:53
@jdesrosiers

Copy link
Copy Markdown
Collaborator

I forgot that the Workspace service hasn't been merged yet and this depends on that feature. Once #32 is merged, you'll need to rebase (don't use merge, use rebase!) and try again. This is on hold for now.

@Suyog241005 Suyog241005 force-pushed the feat/self-identifying-schemas branch from 5e66f7a to 0254094 Compare July 5, 2026 02:59
@Suyog241005

Copy link
Copy Markdown
Contributor Author

Hi @jdesrosiers , Since PR #32 has been merged, I have successfully rebased the branch on top of main, resolved the conflicts, and ensured all tests are green. It is ready for review.

@jdesrosiers jdesrosiers left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Honestly, this needs a lot of work. I left a few comments, but it's just a start and is just code quality issues. I'll take a closer look at the functionality after you clean things up a little.

This functionality shouldn't be in its own class. It needs to be part of the SchemaStore functionality. So, for the next iteration, try to clean things up and move the functionality to SchemaStore.

Comment on lines +307 to +309
// Clear socket queue from duplicate open/watch notifications
await new Promise((resolve) => setTimeout(resolve, 100));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isn't acceptable. Find a way to do this without an arbitrary wait.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove the setTimeout blocks from the tests.

To test the startup discovery without a sleep, would it make sense to have the test client listen for the server's "window/logMessage" (e.g. waiting for "Scanning completed") before triggering the test validation? Or is there a more standard LSP event or testing helper in this codebase that I should use to detect when the server has settled?

afterEach(async () => {
await client.stop();
try {
unregisterSchema(schemaId);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The server should be managing schema registration, not the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove the manual unregisterSchema call from the tests.

To ensure the server cleans up registered workspace schemas when it shuts down, would it make sense to add a server.onShutdown listener inside SchemaStore that unregisters them? Or is there a different server lifecycle hook you would recommend for this?

// 2. Create and open an instance file that references the local schema by its $id
const diagnosticsPromise = new Promise<Diagnostic[]>((resolve) => {
client.onNotification("textDocument/publishDiagnostics", (params: PublishDiagnosticsParams) => {
if (params.uri.endsWith("instance.json")) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

writeDocument returns the file URI. Use that to match on params.uri.

Comment on lines +54 to +56
const diagnostics = await diagnosticsPromise;
expect(diagnostics).toHaveLength(1);
expect(diagnostics[0].message).toContain("Expected a ⁨string⁩");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have a look at similar assertion in SchemaValidation.test.ts and try to match that code style.

Comment on lines +61 to +83
// 3. Update the schema to allow a number for "foo"
const updatedDiagnosticsPromise = new Promise<Diagnostic[]>((resolve) => {
client.onNotification("textDocument/publishDiagnostics", (params: PublishDiagnosticsParams) => {
if (params.uri.endsWith("instance.json")) {
resolve(params.diagnostics);
}
});
});

await client.writeDocument("my-schema.json", `{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "${schemaId}",
"type": "object",
"properties": {
"foo": { "type": "number" }
}
}`);

// Wait a brief moment for the watched file event and write to settle on Windows
await new Promise((resolve) => setTimeout(resolve, 100));

const updatedDiagnostics = await updatedDiagnosticsPromise;
expect(updatedDiagnostics).toHaveLength(0);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test is doing too much. This part should be a separate test.

Comment on lines +24 to +31
server.onInitialize((params) => {
if (params.workspaceFolders) {
this.workspaceFolders = params.workspaceFolders.map((folder) => folder.uri);
} else if (params.rootUri) {
this.workspaceFolders = [params.rootUri];
}
return { capabilities: {} };
});

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Workspace takes care of this. You don't need to do it again. I wanted to wait until the other PR was merged specifically so you don't need to do this.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Understood. I will remove the duplicate onInitialize listener and use this.workspace.workspaceFolders directly.

Comment on lines +40 to +42
if (!fileUri.endsWith(".json") && !fileUri.endsWith(".jsonc")) {
continue;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is unnecessary. The watch was configured to only send .json and .jsonc changes.

Comment on lines +44 to +47
// change.type is: 1 = Created, 2 = Changed, 3 = Deleted
const changeType = change.type as FileChangeType;

if (changeType === 3) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sloppy

Suggested change
// change.type is: 1 = Created, 2 = Changed, 3 = Deleted
const changeType = change.type as FileChangeType;
if (changeType === 3) {
if (change.type === FileChangeType.Deleted) {

try {
const text = await fs.readFile(filePath, "utf-8");
const ast = jsonc.parseTree(text);
if (!ast || ast.type !== "object") {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use optional chaining.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will use optional chaining (ast?.type and schemaNode?.type) for the check.

Comment on lines +116 to +120
try {
unregisterSchema(id);
} catch {
// Ignore if not registered
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unregisterSchema doesn't throw.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will remove the redundant try-catch block and call unregisterSchema(id) directly.

@Suyog241005

Copy link
Copy Markdown
Contributor Author

Hi @jdesrosiers, Thank you for the feedback!

I will consolidate all of the workspace scanning, file watching, and schema registration logic directly into SchemaStore, delete the standalone SelfIdentifyingSchemas class/tests, and address the code quality issues mentioned.

@Suyog241005 Suyog241005 force-pushed the feat/self-identifying-schemas branch from 80c0ca9 to 730ea89 Compare July 7, 2026 03:13
@Suyog241005 Suyog241005 requested a review from jdesrosiers July 7, 2026 03:15
@Suyog241005

Copy link
Copy Markdown
Contributor Author

Hi @jdesrosiers,

I have refactored the PR according to your feedback:

  • Moved all workspace scanning, file watching, and schema registration logic directly into SchemaStore.
  • Removed redundant onInitialize hooks, manual test unregistration, and arbitrary setTimeout waits.
  • Cleaned up the code quality issues and consolidated tests into SchemaValidation.test.ts.

Let me know what you think!

- Implement startup scanning, registration under , and watcher lifecycle management.
- Update  and  cache clearing and dependency mapping with URI normalization.
- Correct  and stabilize test assertions.
@Suyog241005 Suyog241005 force-pushed the feat/self-identifying-schemas branch 2 times, most recently from fce8d6a to d702e53 Compare July 7, 2026 04:22
@Suyog241005 Suyog241005 force-pushed the feat/self-identifying-schemas branch from d702e53 to 9287ee0 Compare July 7, 2026 04:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants