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
2 changes: 1 addition & 1 deletion .claude/scratchpad.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@

## Flaky Tests

- **`packages/cli/src/commands/commands.test.ts:119` — "should display indexing summary without storage size"** times out at 30s on GitHub CI runners. The test indexes files and the slower CI runner can't finish in time. Needs either a higher timeout, a smaller test fixture, or mocking the indexer. Seen on PR #17 CI run.
(none currently tracked)

## Test Gaps

Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/commands/commands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,6 @@ export class Calculator {
// Verify storage size is NOT shown (deferred to `dev stats`)
const hasStorageSize = loggedMessages.some((msg) => msg.includes('Storage:'));
expect(hasStorageSize).toBe(false);
}, 30000); // 30s timeout for indexing
}, 60000); // 60s timeout — ts-morph project init is slow on CI runners
});
});
51 changes: 50 additions & 1 deletion packages/core/src/scanner/__tests__/scanner.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as path from 'node:path';
import { describe, expect, it } from 'vitest';
import { MarkdownScanner } from '../markdown';
import { ScannerRegistry } from '../registry';
import { TypeScriptScanner } from '../typescript';
import { normalizeAndRelativize, TypeScriptScanner } from '../typescript';

// Helper to create registry
function createDefaultRegistry(): ScannerRegistry {
Expand Down Expand Up @@ -1006,3 +1006,52 @@ describe('Scanner', () => {
});
});
});

describe('normalizeAndRelativize', () => {
const repoRoot = '/Users/dev/project';

it('should replace dist/ with src/', () => {
expect(
normalizeAndRelativize('/Users/dev/project/packages/core/dist/map/index.ts', repoRoot)
).toBe('packages/core/src/map/index.ts');
});

it('should replace .d.ts with .ts', () => {
expect(
normalizeAndRelativize('/Users/dev/project/packages/core/dist/types.d.ts', repoRoot)
).toBe('packages/core/src/types.ts');
});

it('should replace .js with .ts', () => {
expect(normalizeAndRelativize('/Users/dev/project/packages/core/dist/index.js', repoRoot)).toBe(
'packages/core/src/index.ts'
);
});

it('should handle multiple dist/ segments', () => {
expect(
normalizeAndRelativize(
'/Users/dev/project/packages/core/dist/formatters/dist/utils.js',
repoRoot
)
).toBe('packages/core/src/formatters/src/utils.ts');
});

it('should make paths relative to repoRoot', () => {
expect(normalizeAndRelativize('/Users/dev/project/packages/cli/src/cli.ts', repoRoot)).toBe(
'packages/cli/src/cli.ts'
);
});

it('should return absolute path when repoRoot is empty', () => {
expect(normalizeAndRelativize('/abs/packages/core/src/index.ts', '')).toBe(
'/abs/packages/core/src/index.ts'
);
});

it('should handle path without dist/', () => {
expect(normalizeAndRelativize('/Users/dev/project/packages/core/src/index.ts', repoRoot)).toBe(
'packages/core/src/index.ts'
);
});
});
67 changes: 52 additions & 15 deletions packages/core/src/scanner/typescript.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import type { Logger } from '@prosdevlab/kero';
import {
Expand All @@ -19,6 +20,21 @@ import {
import { getCurrentSystemResources, getOptimalConcurrency } from '../utils/concurrency';
import type { CalleeInfo, Document, Scanner, ScannerCapabilities } from './types';

/**
* Normalize a resolved file path: dist/ → src/, .d.ts → .ts, absolute → relative.
* Pure function — no I/O.
*/
export function normalizeAndRelativize(filePath: string, repoRoot: string): string {
let normalized = filePath
.replaceAll('/dist/', '/src/')
.replace(/\.d\.ts$/, '.ts')
.replace(/\.js$/, '.ts');
if (repoRoot && normalized.startsWith(repoRoot)) {
normalized = path.relative(repoRoot, normalized);
}
return normalized;
}

/**
* Enhanced TypeScript scanner using ts-morph
* Provides type information and cross-file references
Expand Down Expand Up @@ -987,21 +1003,7 @@ export class TypeScriptScanner implements Scanner {
const declSourceFile = firstDecl.getSourceFile();
if (declSourceFile) {
const rawPath = declSourceFile.getFilePath() as string;
// Only include if it's within the project (not node_modules)
if (rawPath && !rawPath.includes('node_modules')) {
// Normalize: dist/ → src/, .d.ts → .ts, then make relative to repo root.
// ts-morph resolves imports to absolute dist output paths
// (e.g. /abs/packages/logger/dist/types.d.ts) but we store
// relative source paths (packages/logger/src/types.ts).
let normalized = rawPath
.replaceAll('/dist/', '/src/')
.replace(/\.d\.ts$/, '.ts')
.replace(/\.js$/, '.ts');
if (this.repoRoot && normalized.startsWith(this.repoRoot)) {
normalized = path.relative(this.repoRoot, normalized);
}
file = normalized;
}
file = this.normalizeCalleePath(rawPath);
}
}
}
Expand All @@ -1016,4 +1018,39 @@ export class TypeScriptScanner implements Scanner {
file,
};
}

/**
* Normalize a callee file path to a relative source path.
*
* Handles three cases:
* 1. Direct project files (not in node_modules) — normalize dist/ → src/
* 2. Workspace package symlinks (node_modules/@scope/pkg → packages/pkg) — resolve symlink
* 3. External node_modules — skip (return undefined)
*/
private normalizeCalleePath(rawPath: string): string | undefined {
if (!rawPath) return undefined;

// Case 1: Not in node_modules — direct project file
if (!rawPath.includes('node_modules')) {
return normalizeAndRelativize(rawPath, this.repoRoot);
}

// Case 2: Workspace package symlink — resolve to real path
// pnpm workspace links: node_modules/@scope/pkg → ../../actual-pkg
// After resolving, the real path is inside the repo but NOT in node_modules
if (this.repoRoot) {
try {
const realPath = fs.realpathSync(rawPath);
if (realPath.startsWith(this.repoRoot) && !realPath.includes('node_modules')) {
// It's a workspace package — normalize and make relative
return normalizeAndRelativize(realPath, this.repoRoot);
}
} catch {
// realpathSync can fail if the file doesn't exist
}
}

// Case 3: External dependency — skip
return undefined;
}
}
Loading