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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
- `codegraph index` no longer stops with advice to run `codegraph daemon stop`, a command that doesn't exist, when the project's background-server lock names a process that isn't CodeGraph, such as a lock left by a crash whose process number the system has since given to another program: it now clears that stale lock, leaving the process alone, and rebuilds, and `codegraph unlock` clears it too. When a CodeGraph process that doesn't answer holds the lock, such as an older background server, the message names that process and the command that stops it, and the message about another server already serving a project now points to `codegraph daemon`. Thanks @inth3shadows for the report and @danusha2345 for the first fix. (#2770)
- A program that uses CodeGraph as a library and indexes chosen files with `indexFiles()` now keeps other CodeGraph processes, like a `codegraph sync` run by a git hook, out of the index until those files are stored. Before, it let go of the write lock that keeps two writers apart as soon as indexing started, so another process could write to the index at the same time.
- The shared background server is no longer killed with "Main thread unresponsive" while it brings another project's index up to date, such as one a session names with `projectPath`: only its own project's index counted as progress, so a long update of any other looked like a hang, and the server was stopped and restarted again and again. Every index the server keeps up to date now counts, and so does the one a session updates on its own when the background server is unavailable. Thanks @mradwankhalil for the report and the first fix. (#2404)
- A long-running session, like the background server behind your agent, now notices a framework that arrives or goes while it runs, such as Express in a new `package.json`, or one app replaced by another with as many files. Files it indexes from then on are read with the frameworks the project has now; before, they were read with the ones it had when the session started, so a new Express app's routes were missing until a full `codegraph index`. Files indexed before the change are not read again, so `codegraph status` now says which frameworks changed since the last full index and recommends running one. Thanks @nk33-dev for the report and the first fix. (#2516, #2519)
- A long-running session, like the background server behind your agent, now notices a framework that arrives or goes while it runs, such as Express in a new `package.json`, or one app replaced by another with as many files. Files it indexes from then on are read with the frameworks the project has now; before, they were read with the ones it had when the session started, so a new Express app's routes were missing until a full `codegraph index`. Files indexed before the change are not read again, so `codegraph status` now says which frameworks changed since the last full index and recommends running one, including when only `package.json` changed. Thanks @nk33-dev for the report and the first fix. (#2516, #2519)
- On Windows, deleting the folder a link in your project points to, like a junction to a library kept outside the project, no longer makes CodeGraph's server use a full CPU core until it is restarted, or stop picking up your edits. The linked files now drop out of the index while the rest of the project keeps updating, and if the project folder itself disappears while watched, live updates turn off with a message that says so. Thanks @L0garithmic.
- A project a session queries through `projectPath` is now let go once it has gone its own 10 minutes without a query. Before, when its index was still being brought up to date as the session went on to query another project, it was held, locked, until that other project's 10 minutes were up, so its own session and `codegraph index` could wait nearly twice as long to take over. Thanks @bompus. (#2087)
- An index left incomplete by an indexing run that was stopped partway, by a crash, an out-of-memory kill or the server's own hang check, no longer gets the background server killed with "Main thread unresponsive" every time it starts: on a large project, restoring the database indexes the run had set aside took longer than the hang check allows, so each restart died at the same point and the index was never repaired. They are now restored in the background, and the project is served once they are back. Thanks @cbeaulieu-gt for the report and the first fix. (#1887)
Expand Down
103 changes: 103 additions & 0 deletions __tests__/framework-hint-resolver-only.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/**
* `codegraph status` recommends a re-index when a framework arrives or leaves
* through `package.json` alone.
*
* #2516 records the frameworks a full index extracted with, and a sync that
* detects a different set says the files indexed before it lack the change. A
* sync detects again only when it extracts a file, though: add `@sveltejs/kit`
* (or `express`) to `package.json` with no source file edited and nothing is
* recorded, so status stayed silent while `goto('/login')` in an unchanged
* `+layout.svelte` (or an unchanged Express app's routes) stayed unlinked.
*/

import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { CodeGraph } from '../src';

const KIT = JSON.stringify({ name: 'app', devDependencies: { '@sveltejs/kit': '^2.0.0', svelte: '^4.0.0' } });
const NO_KIT = JSON.stringify({ name: 'app', devDependencies: { svelte: '^4.0.0' } });

describe('framework changes since the last full index, from package.json alone', () => {
let root: string;
let cg: CodeGraph | undefined;
const write = (rel: string, content: string) => {
fs.mkdirSync(path.dirname(path.join(root, rel)), { recursive: true });
fs.writeFileSync(path.join(root, rel), content);
};
const navigates = (): number => {
const nodes = cg!.getNodesInFile('src/routes/+layout.svelte');
return cg!.getOutgoingEdgesFrom(nodes.map((n) => n.id)).filter((e) => e.kind === 'navigates').length;
};

beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fw-hint-'));
write('src/routes/+layout.svelte', '<script>\n import { goto } from "$app/navigation";\n function out() { goto("/login"); }\n</script>\n');
write('src/routes/login/+page.svelte', '<h1>login</h1>\n');
});

afterEach(() => {
cg?.destroy();
cg = undefined;
fs.rmSync(root, { recursive: true, force: true });
});

it('names the SvelteKit router when @sveltejs/kit is added after the index', async () => {
write('package.json', NO_KIT);
cg = CodeGraph.initSync(root);
await cg.indexAll();
expect(cg.getFrameworkChangesSinceIndex()).toBeNull();

write('package.json', KIT);
cg.close();
cg = CodeGraph.openSync(root);
await cg.sync();
// The unchanged layout's goto is still unlinked: only a re-index links it...
expect(navigates()).toBe(0);
// ...which is what status must now say.
expect(cg.getFrameworkChangesSinceIndex()?.added).toContain('sveltekit-router');

cg.close();
cg = await CodeGraph.recreate(root);
await cg.indexAll();
expect(navigates()).toBe(1);
expect(cg.getFrameworkChangesSinceIndex()).toBeNull();
});

it('names it as removed when the dependency goes', async () => {
write('package.json', KIT);
cg = CodeGraph.initSync(root);
await cg.indexAll();
expect(navigates()).toBe(1);

write('package.json', NO_KIT);
cg.close();
cg = CodeGraph.openSync(root);
expect(cg.getFrameworkChangesSinceIndex()?.removed).toContain('sveltekit-router');
});

it('names Express when it is added with no source file changed', async () => {
write('package.json', JSON.stringify({ name: 'app', dependencies: {} }));
write('src/server.js', "const express = require('express');\nconst app = express();\napp.get('/hello', (req, res) => res.send('hi'));\n");
cg = CodeGraph.initSync(root);
await cg.indexAll();
expect(cg.getFrameworkChangesSinceIndex()).toBeNull();

write('package.json', JSON.stringify({ name: 'app', dependencies: { express: '^4.0.0' } }));
cg.close();
cg = CodeGraph.openSync(root);
await cg.sync();
expect(cg.getFrameworkChangesSinceIndex()?.added).toContain('express');
});

it('says nothing when the project is as it was indexed', async () => {
write('package.json', KIT);
cg = CodeGraph.initSync(root);
await cg.indexAll();
cg.close();
cg = CodeGraph.openSync(root);
await cg.sync();
expect(cg.getFrameworkChangesSinceIndex()).toBeNull();
});
});
5 changes: 3 additions & 2 deletions src/bin/codegraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1167,8 +1167,9 @@ program

const buildInfo = cg.getIndexBuildInfo();
const reindexRecommended = cg.isIndexStale();
// Frameworks a sync detected since the last full index (#2516): files
// indexed before then were extracted without them.
// Frameworks that came or went since the last full index (#2516), as a
// sync recorded them or as detected now: files indexed before then were
// read without the change.
const frameworkChanges = cg.getFrameworkChangesSinceIndex();
const indexState = cg.getIndexState();
// Zero on a healthy index; non-zero at rest means a resolution pass was
Expand Down
55 changes: 41 additions & 14 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,14 @@ const FRAMEWORKS_AT_INDEX_KEY = 'frameworks_at_index';
* last full index, against {@link FRAMEWORKS_AT_INDEX_KEY}; '' when none.
*/
const FRAMEWORKS_SINCE_INDEX_KEY = 'frameworks_since_index';
/**
* Metadata key: the frameworks (sorted JSON names) the resolver had at the last
* full index. A sync detects again only when it extracts a file, so one that
* finds just `package.json` changed records nothing in
* {@link FRAMEWORKS_SINCE_INDEX_KEY}; the resolver detects on every open, so
* its set is compared against this one as it is now.
*/
const RESOLVER_FRAMEWORKS_AT_INDEX_KEY = 'resolver_frameworks_at_index';

/** A JSON list of names from metadata, or null when absent or unreadable. */
function parseNameList(value: string | null): string[] | null {
Expand Down Expand Up @@ -949,6 +957,7 @@ export class CodeGraph {
try {
this.queries.setMetadata(FRAMEWORKS_AT_INDEX_KEY, JSON.stringify([...names].sort()));
this.queries.setMetadata(FRAMEWORKS_SINCE_INDEX_KEY, '');
this.queries.setMetadata(RESOLVER_FRAMEWORKS_AT_INDEX_KEY, JSON.stringify([...this.resolver.getDetectedFrameworks()].sort()));
} catch { /* metadata is advisory */ }
}

Expand Down Expand Up @@ -980,24 +989,42 @@ export class CodeGraph {
}

/**
* The frameworks a sync detected since the last full index, against the ones
* that index extracted every file with (#2516): files indexed before the
* change were read without the frameworks added, and with the ones removed,
* so a full re-index brings them in line. Null when nothing changed, or for
* an index built before this was recorded.
* The frameworks that came or went since the last full index (#2516): files
* indexed before the change were read without the frameworks added, and with
* the ones removed, so a full re-index brings them in line. Null when nothing
* changed; an index built before a half of this was recorded gives nothing
* for that half.
*
* The first half is what a sync detected against the frameworks the index
* extracted every file with.
*
* A sync that only finds a manifest changed (`express` or `@sveltejs/kit`
* added to `package.json`, no source file edited) extracts nothing, so it
* never detects again and records nothing: the files indexed before it still
* lack the framework. The resolver's set as detected on open, against the
* one the last full index had, covers that.
*/
getFrameworkChangesSinceIndex(): { added: string[]; removed: string[] } | null {
const names = (list: unknown): string[] => (Array.isArray(list) ? list.filter((n): n is string => typeof n === 'string') : []);
const added = new Set<string>();
const removed = new Set<string>();
try {
const value = this.queries.getMetadata(FRAMEWORKS_SINCE_INDEX_KEY);
if (!value) return null;
const parsed = JSON.parse(value) as { added?: unknown; removed?: unknown };
const names = (list: unknown): string[] => (Array.isArray(list) ? list.filter((n): n is string => typeof n === 'string') : []);
const added = names(parsed.added);
const removed = names(parsed.removed);
return added.length > 0 || removed.length > 0 ? { added, removed } : null;
} catch {
return null;
}
if (value) {
const parsed = JSON.parse(value) as { added?: unknown; removed?: unknown };
for (const name of names(parsed.added)) added.add(name);
for (const name of names(parsed.removed)) removed.add(name);
}
} catch { /* metadata is advisory */ }
try {
const atIndex = parseNameList(this.queries.getMetadata(RESOLVER_FRAMEWORKS_AT_INDEX_KEY));
if (atIndex) {
const now = this.resolver.getDetectedFrameworks();
for (const name of now) if (!atIndex.includes(name)) added.add(name);
for (const name of atIndex) if (!now.includes(name)) removed.add(name);
}
} catch { /* metadata is advisory */ }
return added.size > 0 || removed.size > 0 ? { added: [...added].sort(), removed: [...removed].sort() } : null;
}

async sync(options: IndexOptions = {}): Promise<SyncResult> {
Expand Down