mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
fix(ingestion): discover nested source directories (#3043)
This commit is contained in:
parent
9d4f029001
commit
88df18b829
11 changed files with 374 additions and 20 deletions
|
|
@ -1,4 +1,5 @@
|
|||
import ignore, { type Ignore } from 'ignore';
|
||||
import { existsSync } from 'fs';
|
||||
import fs from 'fs/promises';
|
||||
import nodePath from 'path';
|
||||
import type { Path } from 'path-scurry';
|
||||
|
|
@ -31,12 +32,15 @@ const DEFAULT_IGNORE_LIST = new Set([
|
|||
// 'packages' removed - commonly used for monorepo source code (lerna, pnpm, yarn workspaces)
|
||||
'venv',
|
||||
'.venv',
|
||||
'env',
|
||||
'.env',
|
||||
// Bare `env/` can be application source or a Python virtual environment.
|
||||
// Path-aware rules below prune it at the root and wherever pyvenv.cfg marks
|
||||
// a virtual environment, while preserving ordinary nested source folders.
|
||||
'__pycache__',
|
||||
'.pytest_cache',
|
||||
'.mypy_cache',
|
||||
'site-packages',
|
||||
'dist-packages',
|
||||
'.tox',
|
||||
'eggs',
|
||||
'.eggs',
|
||||
|
|
@ -86,8 +90,9 @@ const DEFAULT_IGNORE_LIST = new Set([
|
|||
|
||||
// Generated/Compiled
|
||||
'.generated',
|
||||
'generated',
|
||||
'auto-generated',
|
||||
// Bare `generated/` can contain tracked source-of-truth code. Build output
|
||||
// remains covered by .gitignore/.gitnexusignore and the unambiguous names.
|
||||
'monaco-workers', // Monaco editor web-worker bundles generated for browser runtime
|
||||
'.terraform',
|
||||
'.serverless',
|
||||
|
|
@ -106,6 +111,14 @@ const DEFAULT_IGNORE_LIST = new Set([
|
|||
'__snapshots__',
|
||||
]);
|
||||
|
||||
// Ambiguous names that conventionally denote generated artifacts only at the
|
||||
// repository root. Nested directories with these names are frequently source
|
||||
// modules (for example apps/web/src/env or packages/api/generated).
|
||||
const ROOT_ARTIFACT_DIRECTORIES = new Set(['env', 'generated']);
|
||||
|
||||
const isRootArtifactDirectory = (relativePath: string, name: string): boolean =>
|
||||
!relativePath.includes('/') && ROOT_ARTIFACT_DIRECTORIES.has(name);
|
||||
|
||||
const IGNORED_EXTENSIONS = new Set([
|
||||
// Images
|
||||
'.png',
|
||||
|
|
@ -290,6 +303,10 @@ export const shouldIgnorePath = (filePath: string): boolean => {
|
|||
const fileName = parts[parts.length - 1];
|
||||
const fileNameLower = fileName.toLowerCase();
|
||||
|
||||
if (parts.length > 0 && isRootArtifactDirectory(parts[0], parts[0])) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Laravel compiles Blade templates into generated PHP cache files under
|
||||
// storage/framework/views. Source templates live in resources/views and are
|
||||
// handled separately; compiled cache should not become source-of-truth. Keep
|
||||
|
|
@ -329,10 +346,8 @@ export const shouldIgnorePath = (filePath: string): boolean => {
|
|||
if (
|
||||
fileNameLower.includes('.bundle.') ||
|
||||
fileNameLower.includes('.chunk.') ||
|
||||
fileNameLower.includes('.generated.') ||
|
||||
fileNameLower.endsWith('.d.ts')
|
||||
fileNameLower.includes('.generated.')
|
||||
) {
|
||||
// TypeScript declaration files
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -344,6 +359,20 @@ export const isHardcodedIgnoredDirectory = (name: string): boolean => {
|
|||
return DEFAULT_IGNORE_LIST.has(name);
|
||||
};
|
||||
|
||||
/** Apply directory ignore rules that depend on repository-relative depth. */
|
||||
export const isHardcodedIgnoredDirectoryAtPath = (
|
||||
repoRoot: string,
|
||||
directoryPath: string,
|
||||
): boolean => {
|
||||
const name = nodePath.basename(directoryPath);
|
||||
if (isHardcodedIgnoredDirectory(name)) return true;
|
||||
|
||||
const relative = nodePath.relative(repoRoot, directoryPath).replace(/\\/g, '/');
|
||||
if (isRootArtifactDirectory(relative, name)) return true;
|
||||
|
||||
return name === 'env' && existsSync(nodePath.join(directoryPath, 'pyvenv.cfg'));
|
||||
};
|
||||
|
||||
/**
|
||||
* Load .gitignore and .gitnexusignore rules from the repo root.
|
||||
* Returns an `ignore` instance with all patterns, or null if no files found.
|
||||
|
|
@ -496,8 +525,10 @@ export const createIgnoreFilter = async (repoPath: string, options?: IgnoreOptio
|
|||
// last-match-wins: `!__tests__/` + `__tests__/generated/` still
|
||||
// blocks descent into `__tests__/generated/`.
|
||||
if (ig && rel && hasExplicitUnignore(ig, rel) && !ig.ignores(rel + '/')) return false;
|
||||
// Hardcoded list: block descent into well-known noise directories.
|
||||
if (DEFAULT_IGNORE_LIST.has(p.name)) return true;
|
||||
// Hardcoded and path-aware rules prune whole trees before glob walks them.
|
||||
if (rel && isHardcodedIgnoredDirectoryAtPath(repoPath, nodePath.join(repoPath, rel))) {
|
||||
return true;
|
||||
}
|
||||
// Check against .gitignore / .gitnexusignore patterns.
|
||||
// Since childrenIgnored is only called for directories, always test with
|
||||
// a trailing slash. This ensures directory-only negation patterns (e.g.
|
||||
|
|
|
|||
|
|
@ -2,7 +2,11 @@ import fs from 'node:fs/promises';
|
|||
import path from 'node:path';
|
||||
import type { CypherExecutor } from '../contract-extractor.js';
|
||||
import type { GroupManifestLink, ContractRole } from '../types.js';
|
||||
import { shouldIgnorePath, loadIgnoreRules } from '../../../config/ignore-service.js';
|
||||
import {
|
||||
shouldIgnorePath,
|
||||
loadIgnoreRules,
|
||||
isHardcodedIgnoredDirectoryAtPath,
|
||||
} from '../../../config/ignore-service.js';
|
||||
|
||||
import { logger } from '../../logger.js';
|
||||
interface PythonPackageMeta {
|
||||
|
|
@ -161,9 +165,11 @@ async function findPythonFiles(repoPath: string): Promise<string[]> {
|
|||
for (const entry of entries) {
|
||||
const childRel = rel ? `${rel}/${entry.name}` : entry.name;
|
||||
if (entry.isDirectory()) {
|
||||
const childPath = path.join(dir, entry.name);
|
||||
if (shouldIgnorePath(childRel)) continue;
|
||||
if (isHardcodedIgnoredDirectoryAtPath(repoPath, childPath)) continue;
|
||||
if (ig && ig.ignores(childRel + '/')) continue;
|
||||
await walk(path.join(dir, entry.name), childRel);
|
||||
await walk(childPath, childRel);
|
||||
} else if (entry.name.endsWith('.py')) {
|
||||
if (shouldIgnorePath(childRel)) continue;
|
||||
if (ig && ig.ignores(childRel)) continue;
|
||||
|
|
|
|||
|
|
@ -22,6 +22,27 @@ export interface FilePath {
|
|||
const READ_CONCURRENCY = 32;
|
||||
const ANALYZE_PROGRESS_ACTIVE_ENV = 'GITNEXUS_ANALYZE_PROGRESS_ACTIVE';
|
||||
|
||||
const DECLARATION_COMPANION_SUFFIXES = [
|
||||
{ declaration: '.d.ts', implementations: ['.ts', '.tsx'] },
|
||||
{ declaration: '.d.mts', implementations: ['.mts'] },
|
||||
{ declaration: '.d.cts', implementations: ['.cts'] },
|
||||
] as const;
|
||||
|
||||
const hasImplementationSibling = (
|
||||
declarationPath: string,
|
||||
scannedPaths: ReadonlySet<string>,
|
||||
): boolean => {
|
||||
const companion = DECLARATION_COMPANION_SUFFIXES.find(({ declaration }) =>
|
||||
declarationPath.endsWith(declaration),
|
||||
);
|
||||
if (!companion) return false;
|
||||
|
||||
// Keep standalone declarations. Only suppress declaration output that sits
|
||||
// beside an implementation with the corresponding module suffix.
|
||||
const stem = declarationPath.slice(0, -companion.declaration.length);
|
||||
return companion.implementations.some((suffix) => scannedPaths.has(`${stem}${suffix}`));
|
||||
};
|
||||
|
||||
const warnLargeFileSkip = (message: string): void => {
|
||||
if (process.env[ANALYZE_PROGRESS_ACTIVE_ENV] === '1') {
|
||||
// analyze.ts routes console.warn through the progress bar logger while
|
||||
|
|
@ -84,10 +105,17 @@ export const walkRepositoryPaths = async (
|
|||
}
|
||||
}
|
||||
|
||||
const scannedPaths = new Set(entries.map((entry) => entry.path));
|
||||
const deduplicatedEntries = entries.filter(
|
||||
(entry) => !hasImplementationSibling(entry.path, scannedPaths),
|
||||
);
|
||||
|
||||
// Filesystem/glob traversal order is not stable across filesystems or repeated
|
||||
// scans. Canonicalize once at the scan boundary so every downstream phase sees
|
||||
// the same repository order.
|
||||
entries.sort((left, right) => (left.path < right.path ? -1 : left.path > right.path ? 1 : 0));
|
||||
deduplicatedEntries.sort((left, right) =>
|
||||
left.path < right.path ? -1 : left.path > right.path ? 1 : 0,
|
||||
);
|
||||
|
||||
if (skippedLarge > 0) {
|
||||
const isDefault = maxFileSizeBytes === DEFAULT_MAX_FILE_SIZE_BYTES;
|
||||
|
|
@ -123,7 +151,7 @@ export const walkRepositoryPaths = async (
|
|||
}
|
||||
}
|
||||
|
||||
return entries;
|
||||
return deduplicatedEntries;
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import fs from 'fs/promises';
|
|||
import path from 'path';
|
||||
import { createRequire } from 'node:module';
|
||||
|
||||
import { isHardcodedIgnoredDirectory } from '../../../config/ignore-service.js';
|
||||
import { isHardcodedIgnoredDirectoryAtPath } from '../../../config/ignore-service.js';
|
||||
import { logger } from '../../logger.js';
|
||||
import { resolveFile } from '../languages/typescript/file-candidates.js';
|
||||
|
||||
|
|
@ -361,9 +361,10 @@ export async function loadNodeWorkspacePackages(
|
|||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (isHardcodedIgnoredDirectory(entry.name)) continue;
|
||||
const childDir = path.join(dir, entry.name);
|
||||
if (isHardcodedIgnoredDirectoryAtPath(repoRoot, childDir)) continue;
|
||||
if (depth < SCAN_MAX_DEPTH) {
|
||||
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
||||
queue.push({ dir: childDir, depth: depth + 1 });
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@
|
|||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
import { isHardcodedIgnoredDirectory } from '../../../../config/ignore-service.js';
|
||||
import { isHardcodedIgnoredDirectoryAtPath } from '../../../../config/ignore-service.js';
|
||||
import { logger } from '../../../logger.js';
|
||||
|
||||
/** One `paths` entry, pattern and targets kept in declaration order. */
|
||||
|
|
@ -291,9 +291,9 @@ async function findTsconfigFiles(repoRoot: string): Promise<string[]> {
|
|||
}
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
if (isHardcodedIgnoredDirectory(entry.name)) continue;
|
||||
if (depth < SCAN_MAX_DEPTH)
|
||||
queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
|
||||
const childDir = path.join(dir, entry.name);
|
||||
if (isHardcodedIgnoredDirectoryAtPath(repoRoot, childDir)) continue;
|
||||
if (depth < SCAN_MAX_DEPTH) queue.push({ dir: childDir, depth: depth + 1 });
|
||||
continue;
|
||||
}
|
||||
if (!entry.isFile()) continue;
|
||||
|
|
|
|||
|
|
@ -187,6 +187,125 @@ describe('filesystem-walker', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('ambiguous source-directory names (#3039)', () => {
|
||||
let sourceDir: string;
|
||||
|
||||
beforeAll(async () => {
|
||||
sourceDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-walker-source-names-'));
|
||||
await fs.mkdir(path.join(sourceDir, 'apps', 'client', 'src', 'shared', 'env'), {
|
||||
recursive: true,
|
||||
});
|
||||
await fs.mkdir(path.join(sourceDir, 'packages', 'ai', 'src', 'generated'), {
|
||||
recursive: true,
|
||||
});
|
||||
await fs.mkdir(path.join(sourceDir, 'build-cache', 'generated'), { recursive: true });
|
||||
await fs.mkdir(path.join(sourceDir, 'env'), { recursive: true });
|
||||
await fs.mkdir(path.join(sourceDir, 'generated'), { recursive: true });
|
||||
await fs.mkdir(path.join(sourceDir, 'backend', 'env', 'Scripts'), { recursive: true });
|
||||
await fs.mkdir(path.join(sourceDir, 'backend', 'env', 'include'), { recursive: true });
|
||||
await fs.mkdir(path.join(sourceDir, 'backend', 'env', 'share'), { recursive: true });
|
||||
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'apps', 'client', 'src', 'shared', 'env', 'getAppEnv.ts'),
|
||||
'export const getAppEnv = () => "test";\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'packages', 'ai', 'src', 'generated', 'bundle.ts'),
|
||||
'export const bundled = true;\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'apps', 'client', 'src', 'vite-env.d.ts'),
|
||||
'declare const APP_ENV: string;\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'apps', 'client', 'src', 'service.ts'),
|
||||
'export class UserService {}\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'apps', 'client', 'src', 'service.d.ts'),
|
||||
'export declare class UserService {}\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'apps', 'client', 'src', 'legacy.js'),
|
||||
'export class LegacyService {}\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'apps', 'client', 'src', 'legacy.d.ts'),
|
||||
'export declare class LegacyService {}\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'build-cache', 'generated', 'ignored.ts'),
|
||||
'export const ignored = true;\n',
|
||||
);
|
||||
await fs.writeFile(path.join(sourceDir, '.gitignore'), 'build-cache/generated/\n');
|
||||
await fs.writeFile(path.join(sourceDir, 'env', 'pyvenv.cfg'), 'home = python\n');
|
||||
await fs.writeFile(path.join(sourceDir, 'env', 'settings.py'), 'VALUE = 1\n');
|
||||
await fs.writeFile(path.join(sourceDir, 'backend', 'env', 'pyvenv.cfg'), 'home = python\n');
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'backend', 'env', 'Scripts', 'activate_this.py'),
|
||||
'VALUE = 1\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'backend', 'env', 'include', 'header.py'),
|
||||
'VALUE = 1\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'backend', 'env', 'share', 'manual.py'),
|
||||
'VALUE = 1\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(sourceDir, 'generated', 'client.ts'),
|
||||
'export const generatedClient = true;\n',
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await fs.rm(sourceDir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
it('discovers nested env/generated and .d.ts source while pruning root artifacts', async () => {
|
||||
const files = await walkRepositoryPaths(sourceDir);
|
||||
const paths = files.map((file) => file.path);
|
||||
|
||||
expect(paths).toContain('apps/client/src/shared/env/getAppEnv.ts');
|
||||
expect(paths).toContain('packages/ai/src/generated/bundle.ts');
|
||||
expect(paths).toContain('apps/client/src/vite-env.d.ts');
|
||||
expect(paths).toContain('apps/client/src/service.ts');
|
||||
expect(paths).not.toContain('apps/client/src/service.d.ts');
|
||||
expect(paths).toContain('apps/client/src/legacy.js');
|
||||
expect(paths).toContain('apps/client/src/legacy.d.ts');
|
||||
expect(paths).not.toContain('build-cache/generated/ignored.ts');
|
||||
expect(paths).not.toContain('env/settings.py');
|
||||
expect(paths).not.toContain('backend/env/Scripts/activate_this.py');
|
||||
expect(paths).not.toContain('backend/env/include/header.py');
|
||||
expect(paths).not.toContain('backend/env/share/manual.py');
|
||||
expect(paths).not.toContain('generated/client.ts');
|
||||
});
|
||||
|
||||
it('preserves case variants that were not hardcoded ignore names', async () => {
|
||||
const caseDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gn-walker-source-case-'));
|
||||
try {
|
||||
await fs.mkdir(path.join(caseDir, 'Generated'), { recursive: true });
|
||||
await fs.mkdir(path.join(caseDir, 'Env'), { recursive: true });
|
||||
await fs.writeFile(
|
||||
path.join(caseDir, 'Generated', 'client.cs'),
|
||||
'public class GeneratedClient {}\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(caseDir, 'Env', 'settings.ts'),
|
||||
'export const environment = "test";\n',
|
||||
);
|
||||
|
||||
const paths = (await walkRepositoryPaths(caseDir)).map((file) => file.path);
|
||||
|
||||
expect(paths).toContain('Generated/client.cs');
|
||||
expect(paths).toContain('Env/settings.ts');
|
||||
} finally {
|
||||
await fs.rm(caseDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('.gitnexusignore support', () => {
|
||||
let nexusignoreDir: string;
|
||||
|
||||
|
|
@ -394,6 +513,7 @@ describe('filesystem-walker', () => {
|
|||
describe('large file skip threshold (#991)', () => {
|
||||
let sizeDir: string;
|
||||
const BIG_FILE = 'src/big.ts';
|
||||
const BIG_DECLARATION = 'src/big.d.ts';
|
||||
const BIG_FILE_BYTES = 600 * 1024;
|
||||
const ORIGINAL_ENV = process.env.GITNEXUS_MAX_FILE_SIZE;
|
||||
let cap: ReturnType<typeof _captureLogger>;
|
||||
|
|
@ -403,6 +523,10 @@ describe('filesystem-walker', () => {
|
|||
await fs.mkdir(path.join(sizeDir, 'src'), { recursive: true });
|
||||
await fs.writeFile(path.join(sizeDir, 'src', 'small.ts'), 'export const x = 1;');
|
||||
await fs.writeFile(path.join(sizeDir, BIG_FILE), 'x'.repeat(BIG_FILE_BYTES));
|
||||
await fs.writeFile(
|
||||
path.join(sizeDir, BIG_DECLARATION),
|
||||
'export declare const generatedTypes: string;\n',
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
|
|
@ -429,6 +553,7 @@ describe('filesystem-walker', () => {
|
|||
const paths = files.map((f) => f.path.replace(/\\/g, '/'));
|
||||
expect(paths).toContain('src/small.ts');
|
||||
expect(paths).not.toContain(BIG_FILE);
|
||||
expect(paths).toContain(BIG_DECLARATION);
|
||||
});
|
||||
|
||||
it('includes the 600KB file when GITNEXUS_MAX_FILE_SIZE=1024', async () => {
|
||||
|
|
@ -436,6 +561,7 @@ describe('filesystem-walker', () => {
|
|||
const files = await walkRepositoryPaths(sizeDir);
|
||||
const paths = files.map((f) => f.path.replace(/\\/g, '/'));
|
||||
expect(paths).toContain(BIG_FILE);
|
||||
expect(paths).not.toContain(BIG_DECLARATION);
|
||||
});
|
||||
|
||||
it('falls back to default and warns once on invalid GITNEXUS_MAX_FILE_SIZE', async () => {
|
||||
|
|
|
|||
|
|
@ -40,6 +40,36 @@ describe('ignore + language-skip E2E', () => {
|
|||
path.join(tmpDir, 'src', 'greet.ts'),
|
||||
"export function greet(): string {\n return 'hello';\n}\n",
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'src', 'service.ts'),
|
||||
'export class UserService { load(): string { return "loaded"; } }\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'src', 'service.d.ts'),
|
||||
'export declare class UserService { load(): string; }\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'src', 'vite-env.d.ts'),
|
||||
'declare const APP_ENV: string;\n',
|
||||
);
|
||||
await fs.writeFile(path.join(tmpDir, 'src', 'esm-service.mts'), 'export class EsmService {}\n');
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'src', 'esm-service.d.mts'),
|
||||
'export declare class EsmService {}\n',
|
||||
);
|
||||
await fs.writeFile(path.join(tmpDir, 'src', 'cjs-service.cts'), 'export class CjsService {}\n');
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'src', 'cjs-service.d.cts'),
|
||||
'export declare class CjsService {}\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'src', 'ambient.d.mts'),
|
||||
'export declare class AmbientEsmService {}\n',
|
||||
);
|
||||
await fs.writeFile(
|
||||
path.join(tmpDir, 'src', 'ambient.d.cts'),
|
||||
'export declare class AmbientCjsService {}\n',
|
||||
);
|
||||
|
||||
// Swift file — triggers language skip when grammar unavailable
|
||||
await fs.writeFile(
|
||||
|
|
@ -70,6 +100,15 @@ describe('ignore + language-skip E2E', () => {
|
|||
|
||||
expect(paths).toContain('src/index.ts');
|
||||
expect(paths).toContain('src/greet.ts');
|
||||
expect(paths).toContain('src/service.ts');
|
||||
expect(paths).not.toContain('src/service.d.ts');
|
||||
expect(paths).toContain('src/vite-env.d.ts');
|
||||
expect(paths).toContain('src/esm-service.mts');
|
||||
expect(paths).not.toContain('src/esm-service.d.mts');
|
||||
expect(paths).toContain('src/cjs-service.cts');
|
||||
expect(paths).not.toContain('src/cjs-service.d.cts');
|
||||
expect(paths).toContain('src/ambient.d.mts');
|
||||
expect(paths).toContain('src/ambient.d.cts');
|
||||
});
|
||||
|
||||
it('includes .swift files (discovery does not filter by language)', async () => {
|
||||
|
|
@ -130,6 +169,30 @@ describe('ignore + language-skip E2E', () => {
|
|||
expect(functionNames).toContain('main');
|
||||
expect(functionNames).toContain('greet');
|
||||
|
||||
const userServiceNodes = nodes.filter(
|
||||
(node) => node.label === 'Class' && node.properties.name === 'UserService',
|
||||
);
|
||||
expect(userServiceNodes).toHaveLength(1);
|
||||
expect(userServiceNodes[0].properties.filePath).toBe('src/service.ts');
|
||||
expect(nodes.some((node) => node.properties.filePath === 'src/service.d.ts')).toBe(false);
|
||||
|
||||
expect(
|
||||
nodes.filter((node) => node.label === 'Class' && node.properties.name === 'EsmService'),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
nodes.filter((node) => node.label === 'Class' && node.properties.name === 'CjsService'),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
nodes.filter(
|
||||
(node) => node.label === 'Class' && node.properties.name === 'AmbientEsmService',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
expect(
|
||||
nodes.filter(
|
||||
(node) => node.label === 'Class' && node.properties.name === 'AmbientCjsService',
|
||||
),
|
||||
).toHaveLength(1);
|
||||
|
||||
// Function nodes should reference the correct source files
|
||||
const fnFilePaths = functionNodes.map((n) =>
|
||||
(n.properties.filePath as string).replace(/\\/g, '/'),
|
||||
|
|
|
|||
|
|
@ -52,6 +52,31 @@ describe('PythonWorkspaceExtractor', () => {
|
|||
});
|
||||
});
|
||||
|
||||
it('does not emit contracts from a nested Python virtual environment', async () => {
|
||||
await writeFile(
|
||||
'provider/pyproject.toml',
|
||||
'[project]\nname = "provider"\nversion = "0.1.0"\ndependencies = []\n',
|
||||
);
|
||||
await writeFile('provider/provider/__init__.py', 'class SecretClient: pass\n');
|
||||
|
||||
await writeFile(
|
||||
'consumer/pyproject.toml',
|
||||
'[project]\nname = "consumer"\nversion = "0.1.0"\ndependencies = ["provider"]\n',
|
||||
);
|
||||
await writeFile('consumer/backend/env/pyvenv.cfg', 'home = python\n');
|
||||
await writeFile('consumer/backend/env/leaked.py', 'from provider import SecretClient\n');
|
||||
|
||||
const repos = { provider: 'provider', consumer: 'consumer' };
|
||||
const repoPaths = new Map([
|
||||
['provider', path.join(tmpDir, 'provider')],
|
||||
['consumer', path.join(tmpDir, 'consumer')],
|
||||
]);
|
||||
|
||||
const result = await extractPythonWorkspaceLinks(repos, repoPaths);
|
||||
|
||||
expect(result.links).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('discovers imports via setup.py', async () => {
|
||||
await writeFile(
|
||||
'core/setup.py',
|
||||
|
|
|
|||
|
|
@ -204,8 +204,8 @@ describe('shouldIgnorePath', () => {
|
|||
expect(shouldIgnorePath('keep-ui/public/monaco-workers/125.js')).toBe(true);
|
||||
});
|
||||
|
||||
it('ignores TypeScript declaration files', () => {
|
||||
expect(shouldIgnorePath('types/index.d.ts')).toBe(true);
|
||||
it('keeps tracked TypeScript declaration files discoverable', () => {
|
||||
expect(shouldIgnorePath('types/index.d.ts')).toBe(false);
|
||||
});
|
||||
|
||||
it('ignores Laravel compiled Blade view cache files', () => {
|
||||
|
|
@ -226,6 +226,12 @@ describe('shouldIgnorePath', () => {
|
|||
it.each([
|
||||
'src/index.ts',
|
||||
'src/components/Button.tsx',
|
||||
'apps/client/src/shared/env/getAppEnv.ts',
|
||||
'packages/ai/src/generated/bundle.ts',
|
||||
'apps/client/src/vite-env.d.ts',
|
||||
'Generated/client.cs',
|
||||
'Env/settings.ts',
|
||||
'ENV/config.ts',
|
||||
'lib/utils.py',
|
||||
'cmd/server/main.go',
|
||||
'src/main.rs',
|
||||
|
|
@ -238,6 +244,13 @@ describe('shouldIgnorePath', () => {
|
|||
])('does not ignore source file %s', (filePath) => {
|
||||
expect(shouldIgnorePath(filePath)).toBe(false);
|
||||
});
|
||||
|
||||
it.each(['env/pyvenv.cfg', 'env/settings.py', 'generated/client.ts'])(
|
||||
'prunes ambiguous artifact directories only at the repository root: %s',
|
||||
(filePath) => {
|
||||
expect(shouldIgnorePath(filePath)).toBe(true);
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -248,6 +261,7 @@ describe('isHardcodedIgnoredDirectory', () => {
|
|||
expect(isHardcodedIgnoredDirectory('dist')).toBe(true);
|
||||
expect(isHardcodedIgnoredDirectory('monaco-workers')).toBe(true);
|
||||
expect(isHardcodedIgnoredDirectory('__pycache__')).toBe(true);
|
||||
expect(isHardcodedIgnoredDirectory('dist-packages')).toBe(true);
|
||||
});
|
||||
|
||||
it('returns false for source directories', () => {
|
||||
|
|
@ -255,6 +269,8 @@ describe('isHardcodedIgnoredDirectory', () => {
|
|||
expect(isHardcodedIgnoredDirectory('lib')).toBe(false);
|
||||
expect(isHardcodedIgnoredDirectory('app')).toBe(false);
|
||||
expect(isHardcodedIgnoredDirectory('local')).toBe(false);
|
||||
expect(isHardcodedIgnoredDirectory('env')).toBe(false);
|
||||
expect(isHardcodedIgnoredDirectory('generated')).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -308,6 +324,33 @@ describe('.gitnexusignore negation overrides hardcoded DEFAULT_IGNORE_LIST (#771
|
|||
expect(filter.childrenIgnored(mkPath('__tests__'))).toBe(true);
|
||||
});
|
||||
|
||||
it('prunes exact-case root artifacts while allowing nested source directories', async () => {
|
||||
const filter = await createIgnoreFilter(tmpDir);
|
||||
|
||||
expect(filter.childrenIgnored(mkPath('generated'))).toBe(true);
|
||||
expect(filter.childrenIgnored(mkPath('env'))).toBe(true);
|
||||
expect(filter.childrenIgnored(mkPath('packages/api/generated'))).toBe(false);
|
||||
expect(filter.childrenIgnored(mkPath('Generated'))).toBe(false);
|
||||
expect(filter.childrenIgnored(mkPath('Env'))).toBe(false);
|
||||
});
|
||||
|
||||
it('prunes a nested env directory only when pyvenv.cfg identifies a virtual environment', async () => {
|
||||
await fs.mkdir(path.join(tmpDir, 'backend', 'env'), { recursive: true });
|
||||
await fs.writeFile(path.join(tmpDir, 'backend', 'env', 'pyvenv.cfg'), 'home = python\n');
|
||||
const filter = await createIgnoreFilter(tmpDir);
|
||||
|
||||
expect(filter.childrenIgnored(mkPath('backend/env'))).toBe(true);
|
||||
expect(filter.childrenIgnored(mkPath('services/api/env'))).toBe(false);
|
||||
});
|
||||
|
||||
it('`!env/` negation unlocks the root artifact directory', async () => {
|
||||
await fs.writeFile(path.join(tmpDir, '.gitnexusignore'), '!env/\n');
|
||||
const filter = await createIgnoreFilter(tmpDir);
|
||||
|
||||
expect(filter.childrenIgnored(mkPath('env'))).toBe(false);
|
||||
expect(filter.ignored(mkPath('env/settings.py'))).toBe(false);
|
||||
});
|
||||
|
||||
it('`!__tests__/` negation unlocks the directory and its descendants', async () => {
|
||||
await fs.writeFile(path.join(tmpDir, '.gitnexusignore'), '!__tests__/\n');
|
||||
const filter = await createIgnoreFilter(tmpDir);
|
||||
|
|
|
|||
|
|
@ -90,6 +90,22 @@ describe('workspace boundary', () => {
|
|||
expect(packages?.byName.has('@repo/web')).toBe(true);
|
||||
});
|
||||
|
||||
it('prunes root artifact workspaces while keeping nested source directories', async () => {
|
||||
const root = repo({
|
||||
'package.json': JSON.stringify({
|
||||
name: 'root',
|
||||
workspaces: ['generated/*', 'packages/*/generated'],
|
||||
}),
|
||||
'generated/apiclient/package.json': pkg('@repo/root-artifact'),
|
||||
'packages/api/generated/package.json': pkg('@repo/generated-source'),
|
||||
});
|
||||
|
||||
const packages = await loadNodeWorkspacePackages(root);
|
||||
|
||||
expect(packages?.byName.has('@repo/root-artifact')).toBe(false);
|
||||
expect(packages?.byName.has('@repo/generated-source')).toBe(true);
|
||||
});
|
||||
|
||||
it('honours a `!` exclusion', async () => {
|
||||
const root = repo({
|
||||
'pnpm-workspace.yaml': 'packages:\n - "packages/*"\n - "!packages/internal"\n',
|
||||
|
|
|
|||
|
|
@ -153,6 +153,21 @@ describe('extends chains', () => {
|
|||
});
|
||||
|
||||
describe('which config governs a file', () => {
|
||||
it('prunes root artifact configs while keeping nested source directories', async () => {
|
||||
const root = repo({
|
||||
'generated/tsconfig.json': JSON.stringify({ compilerOptions: { baseUrl: 'root-artifact' } }),
|
||||
'packages/api/generated/tsconfig.json': JSON.stringify({
|
||||
compilerOptions: { baseUrl: 'src' },
|
||||
}),
|
||||
});
|
||||
const index = await loadTsconfigIndex(root);
|
||||
|
||||
expect(tsconfigFor(index, 'generated/main.ts')).toBeNull();
|
||||
expect(tsconfigFor(index, 'packages/api/generated/main.ts')?.baseUrl).toBe(
|
||||
'packages/api/generated/src',
|
||||
);
|
||||
});
|
||||
|
||||
it('lets a child config with no baseUrl shadow the root, rather than inheriting it', async () => {
|
||||
// The child project declares no `baseUrl`, which in TypeScript means its
|
||||
// non-relative specifiers are PACKAGE lookups. Dropping the empty child let
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue