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
31 changes: 31 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,37 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
grammar, and `web/core`, `web/modules/contrib`, `web/themes/contrib` are
excluded by default. Resolves [#268](https://github.com/colbymchenry/codegraph/issues/268).

### Changed
- **Zero-config indexing that respects `.gitignore`.** CodeGraph no longer has a
config file. It indexes every file whose extension maps to a supported language
and honors your `.gitignore` everywhere: in git repos via git itself, and in
non-git projects (e.g. a freshly-scaffolded app before `git init`) by reading
`.gitignore` files directly — root and nested, the same way git does (via the
`ignore` library, so negation/anchoring/nested rules all behave correctly). To
keep something out of the graph, add it to `.gitignore`. **Behavior change:**
committed files that are *not* gitignored are now indexed even under `vendor/`,
`Pods/`, or a committed `dist/` — previously a hardcoded exclude list skipped
those names; now `.gitignore` is the single source of truth. Resolves
[#283](https://github.com/colbymchenry/codegraph/issues/283).

### Removed
- **`.codegraph/config.json` and the entire config surface.** Every field was
either inert or now redundant with `.gitignore`:
- `languages`/`frameworks` never affected indexing (languages are detected per
file from extensions; frameworks are auto-detected). `languages` was also
broken — its validator only knew the original 8 languages, so setting it to
anything newer (C#, PHP, Ruby, C/C++, Swift, Kotlin, Dart, Vue, Scala, Lua, …)
threw `Invalid configuration format`.
- `extractDocstrings`/`trackCallSites`/`customPatterns` were never read by any
extractor.
- `include` is now derived from the supported language extensions, `exclude` is
replaced by `.gitignore`, and `maxFileSize` (1 MB) is a constant.

**Breaking (library API):** the `CodeGraphConfig` type, the `config` option on
`CodeGraph.init()`, and the `getConfig()`/`updateConfig()`/`getConfigPath`
exports are gone. Existing `.codegraph/config.json` files are simply ignored.
The `.codegraphignore` marker is no longer supported — use `.gitignore`.

## [0.9.1] - 2026-05-21

### Fixed
Expand Down
39 changes: 17 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -418,28 +418,23 @@ cg.close();

## Configuration

The `.codegraph/config.json` file controls indexing:

```json
{
"version": 1,
"languages": ["typescript", "javascript"],
"exclude": ["node_modules/**", "dist/**", "build/**", "*.min.js"],
"frameworks": [],
"maxFileSize": 1048576,
"extractDocstrings": true,
"trackCallSites": true
}
```

| Option | Description | Default |
|--------|-------------|---------|
| `languages` | Languages to index (auto-detected if empty) | `[]` |
| `exclude` | Glob patterns to ignore | `["node_modules/**", ...]` |
| `frameworks` | Framework hints for better resolution | `[]` |
| `maxFileSize` | Skip files larger than this (bytes) | `1048576` (1MB) |
| `extractDocstrings` | Extract docstrings from code | `true` |
| `trackCallSites` | Track call site locations | `true` |
There isn't any — CodeGraph is zero-config. It indexes every file whose
extension maps to a [supported language](#supported-languages) and **respects
your `.gitignore`**: in git repos via git itself, and in non-git projects by
reading `.gitignore` files directly (root and nested, the same way git would).

What that means in practice:

- Anything git ignores — `node_modules`, build output, secrets in `.env` — is
never indexed. **To keep something out of the graph, add it to `.gitignore`.**
- There's no config file to write or keep in sync, and nothing to wire up per
language: support is automatic from the file extension.
- Files larger than 1 MB are skipped (generated bundles, minified JS, vendored
blobs) — they cost parse budget for no useful symbols.

> Committed files that aren't gitignored *are* indexed, even under `vendor/` or a
> committed `dist/`. If you commit a dependency or build directory you don't want
> in the graph, add it to `.gitignore`.

## Supported Languages

Expand Down
70 changes: 33 additions & 37 deletions __tests__/extraction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,9 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { extractFromSource, scanDirectory, shouldIncludeFile } from '../src/extraction';
import { extractFromSource, scanDirectory } from '../src/extraction';
import { detectLanguage, isLanguageSupported, getSupportedLanguages, initGrammars, loadAllGrammars } from '../src/extraction/grammars';
import { normalizePath } from '../src/utils';
import { DEFAULT_CONFIG } from '../src/types';

beforeAll(async () => {
await initGrammars();
Expand Down Expand Up @@ -3003,48 +3002,65 @@ describe('Directory Exclusion', () => {
cleanupTempDir(tempDir);
});

it('should exclude node_modules directories', () => {
// Create structure: src/index.ts + node_modules/pkg/index.js
it('should exclude directories listed in .gitignore', () => {
// Create structure: src/index.ts + node_modules/pkg/index.js, gitignore node_modules
const srcDir = path.join(tempDir, 'src');
const nmDir = path.join(tempDir, 'node_modules', 'pkg');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(nmDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n');

const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);
const files = scanDirectory(tempDir);

expect(files).toContain('src/index.ts');
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
});

it('should exclude nested node_modules directories', () => {
// Create structure: packages/app/node_modules/pkg/index.js
it('should exclude nested node_modules via a root .gitignore', () => {
// A trailing-slash pattern with no leading slash matches at any depth.
const srcDir = path.join(tempDir, 'packages', 'app', 'src');
const nmDir = path.join(tempDir, 'packages', 'app', 'node_modules', 'pkg');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(nmDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
fs.writeFileSync(path.join(nmDir, 'index.js'), 'module.exports = {};');
fs.writeFileSync(path.join(tempDir, '.gitignore'), 'node_modules/\n');

const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);
const files = scanDirectory(tempDir);

expect(files).toContain('packages/app/src/index.ts');
expect(files.every((f) => !f.includes('node_modules'))).toBe(true);
});

it('should exclude .git directories', () => {
it('should apply a nested .gitignore only to its own subtree', () => {
const appSrc = path.join(tempDir, 'app', 'src');
fs.mkdirSync(appSrc, { recursive: true });
fs.writeFileSync(path.join(appSrc, 'keep.ts'), 'export const a = 1;');
fs.writeFileSync(path.join(appSrc, 'skip.ts'), 'export const b = 2;');
fs.writeFileSync(path.join(tempDir, 'app', '.gitignore'), 'src/skip.ts\n');
// A sibling with the same name outside app/ must NOT be ignored.
const otherDir = path.join(tempDir, 'other', 'src');
fs.mkdirSync(otherDir, { recursive: true });
fs.writeFileSync(path.join(otherDir, 'skip.ts'), 'export const c = 3;');

const files = scanDirectory(tempDir);

expect(files).toContain('app/src/keep.ts');
expect(files).not.toContain('app/src/skip.ts');
expect(files).toContain('other/src/skip.ts');
});

it('should always skip .git directories', () => {
const srcDir = path.join(tempDir, 'src');
const gitDir = path.join(tempDir, '.git', 'objects');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(gitDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
fs.writeFileSync(path.join(gitDir, 'pack.ts'), 'export const y = 2;');

const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);
const files = scanDirectory(tempDir);

expect(files).toContain('src/index.ts');
expect(files.every((f) => !f.includes('.git'))).toBe(true);
Expand All @@ -3055,29 +3071,12 @@ describe('Directory Exclusion', () => {
fs.mkdirSync(srcDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'Button.tsx'), 'export function Button() {}');

const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);
const files = scanDirectory(tempDir);

expect(files.length).toBe(1);
expect(files[0]).toBe('src/components/Button.tsx');
expect(files[0]).not.toContain('\\');
});

it('should respect .codegraphignore marker', () => {
const srcDir = path.join(tempDir, 'src');
const vendorDir = path.join(tempDir, 'vendor');
fs.mkdirSync(srcDir, { recursive: true });
fs.mkdirSync(vendorDir, { recursive: true });
fs.writeFileSync(path.join(srcDir, 'index.ts'), 'export const x = 1;');
fs.writeFileSync(path.join(vendorDir, 'lib.ts'), 'export const y = 2;');
fs.writeFileSync(path.join(vendorDir, '.codegraphignore'), '');

const config = { ...DEFAULT_CONFIG, rootDir: tempDir };
const files = scanDirectory(tempDir, config);

expect(files).toContain('src/index.ts');
expect(files.every((f) => !f.includes('vendor'))).toBe(true);
});
});

describe('Git Submodules', () => {
Expand Down Expand Up @@ -3124,8 +3123,7 @@ describe('Git Submodules', () => {
);
git(mainDir, 'commit', '-q', '-m', 'add submodule');

const config = { ...DEFAULT_CONFIG, rootDir: mainDir };
const files = scanDirectory(mainDir, config);
const files = scanDirectory(mainDir);

expect(files).toContain('app.ts');
expect(files).toContain('libs/lib/lib.ts');
Expand Down Expand Up @@ -3173,8 +3171,7 @@ describe('Nested non-submodule git repos', () => {
git(path.join(root, 'sub_repo2'), 'init', '-q');
fs.writeFileSync(path.join(sub2, 'two.ts'), 'export const two = 2;');

const config = { ...DEFAULT_CONFIG, rootDir: root };
const files = scanDirectory(root, config);
const files = scanDirectory(root);

// Both committed and untracked source from the nested repos must be found.
expect(files).toContain('sub_repo1/src/one.ts');
Expand All @@ -3197,8 +3194,7 @@ describe('Nested non-submodule git repos', () => {
fs.writeFileSync(path.join(sub, 'real.ts'), 'export const real = 1;');
fs.writeFileSync(path.join(sub, 'generated.ts'), 'export const generated = 1;');

const config = { ...DEFAULT_CONFIG, rootDir: root };
const files = scanDirectory(root, config);
const files = scanDirectory(root);

expect(files).toContain('sub_repo/src/real.ts');
expect(files).not.toContain('sub_repo/src/generated.ts');
Expand Down
68 changes: 1 addition & 67 deletions __tests__/foundation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,7 @@ import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import { CodeGraph } from '../src';
import { DEFAULT_CONFIG, Node, Edge } from '../src/types';
import { loadConfig, saveConfig } from '../src/config';
import { Node, Edge } from '../src/types';
import { isInitialized, getCodeGraphDir, validateDirectory } from '../src/directory';
import { DatabaseConnection, getDatabasePath } from '../src/db';

Expand Down Expand Up @@ -60,41 +59,12 @@ describe('CodeGraph Foundation', () => {
cg.close();
});

it('should create config.json with defaults', () => {
const cg = CodeGraph.initSync(tempDir);

const configPath = path.join(getCodeGraphDir(tempDir), 'config.json');
expect(fs.existsSync(configPath)).toBe(true);

const config = cg.getConfig();
expect(config.version).toBe(DEFAULT_CONFIG.version);
expect(config.include).toEqual(DEFAULT_CONFIG.include);
expect(config.exclude).toEqual(DEFAULT_CONFIG.exclude);

cg.close();
});

it('should throw if already initialized', () => {
const cg = CodeGraph.initSync(tempDir);
cg.close();

expect(() => CodeGraph.initSync(tempDir)).toThrow(/already initialized/i);
});

it('should accept custom config options', () => {
const cg = CodeGraph.initSync(tempDir, {
config: {
maxFileSize: 500000,
extractDocstrings: false,
},
});

const config = cg.getConfig();
expect(config.maxFileSize).toBe(500000);
expect(config.extractDocstrings).toBe(false);

cg.close();
});
});

describe('Opening Projects', () => {
Expand All @@ -112,17 +82,6 @@ describe('CodeGraph Foundation', () => {
it('should throw if not initialized', () => {
expect(() => CodeGraph.openSync(tempDir)).toThrow(/not initialized/i);
});

it('should preserve configuration across open/close', () => {
const cg1 = CodeGraph.initSync(tempDir, {
config: { maxFileSize: 123456 },
});
cg1.close();

const cg2 = CodeGraph.openSync(tempDir);
expect(cg2.getConfig().maxFileSize).toBe(123456);
cg2.close();
});
});

describe('Static Methods', () => {
Expand Down Expand Up @@ -182,31 +141,6 @@ describe('CodeGraph Foundation', () => {
});
});

describe('Configuration', () => {
it('should load and merge config with defaults', () => {
const cg = CodeGraph.initSync(tempDir);
cg.close();

const config = loadConfig(tempDir);
expect(config.version).toBe(DEFAULT_CONFIG.version);
expect(config.rootDir).toBe(path.resolve(tempDir));
});

it('should update configuration', () => {
const cg = CodeGraph.initSync(tempDir);

cg.updateConfig({ maxFileSize: 999999 });

expect(cg.getConfig().maxFileSize).toBe(999999);

cg.close();

// Verify persistence
const config = loadConfig(tempDir);
expect(config.maxFileSize).toBe(999999);
});
});

describe('Directory Management', () => {
it('should validate directory structure', () => {
const cg = CodeGraph.initSync(tempDir);
Expand Down
Loading