fix: resolve CI test failures
Some checks failed
CI / test (push) Has been cancelled

This commit is contained in:
2026-02-01 01:20:58 +00:00
parent a0ffe2a602
commit 39448d277b

View File

@@ -0,0 +1,353 @@
import * as fs from 'fs';
import * as path from 'path';
import { DependencyInfo, Dependency } from '../types';
interface DependencyFile {
path: string;
parser: (content: string) => Dependency[];
type: 'prod' | 'dev' | 'both';
}
export class DependencyAnalyzer {
private dependencyFiles: DependencyFile[] = [
{
path: 'package.json',
parser: this.parsePackageJson.bind(this),
type: 'both',
},
{
path: 'requirements.txt',
parser: this.parseRequirementsTxt.bind(this),
type: 'prod',
},
{
path: 'pyproject.toml',
parser: this.parsePyprojectToml.bind(this),
type: 'both',
},
{
path: 'go.mod',
parser: this.parseGoMod.bind(this),
type: 'prod',
},
{
path: 'Cargo.toml',
parser: this.parseCargoToml.bind(this),
type: 'both',
},
{
path: 'Pipfile',
parser: this.parsePipfile.bind(this),
type: 'both',
},
{
path: 'Gemfile',
parser: this.parseGemfile.bind(this),
type: 'prod',
},
{
path: 'composer.json',
parser: this.parseComposerJson.bind(this),
type: 'both',
},
];
async analyze(
dir: string,
includeDev: boolean = false
): Promise<DependencyInfo> {
const allDependencies: Dependency[] = [];
for (const depFile of this.dependencyFiles) {
const filePath = path.join(dir, depFile.path);
if (await this.fileExists(filePath)) {
try {
const content = await fs.promises.readFile(filePath, 'utf-8');
const deps = depFile.parser(content);
if (depFile.type === 'both') {
allDependencies.push(...deps);
} else if (depFile.type === 'prod' || includeDev) {
allDependencies.push(...deps);
}
} catch (error) {
console.warn(`Failed to parse ${depFile.path}: ${error}`);
}
}
}
const direct = allDependencies.filter(d => !d.isLocal);
const dev = allDependencies.filter(d => d.type === 'dev');
return {
direct,
dev,
total: direct.length + dev.length,
};
}
private parsePackageJson(content: string): Dependency[] {
const packageJson = JSON.parse(content);
const dependencies: Dependency[] = [];
const parseDeps = (
deps: Record<string, string> | undefined,
type: 'prod' | 'dev'
) => {
if (!deps) return;
for (const [name, version] of Object.entries(deps)) {
dependencies.push({
name,
version: version as string,
type,
isLocal: name.startsWith('.') || name.startsWith('/') || name.startsWith('@'),
});
}
};
parseDeps(packageJson.dependencies, 'prod');
parseDeps(packageJson.devDependencies, 'dev');
return dependencies;
}
private parseRequirementsTxt(content: string): Dependency[] {
const lines = content.split('\n');
const dependencies: Dependency[] = [];
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#') || trimmed.startsWith('-')) continue;
const match = trimmed.match(/^([a-zA-Z0-9_-]+)([<>=!~]+)(.+)$/);
if (match) {
dependencies.push({
name: match[1],
version: match[2] + match[3],
type: 'prod',
isLocal: false,
});
} else if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
dependencies.push({
name: trimmed,
version: '*',
type: 'prod',
isLocal: false,
});
}
}
return dependencies;
}
private parsePyprojectToml(content: string): Dependency[] {
const dependencies: Dependency[] = [];
const lines = content.split('\n');
let inDependencies = false;
let inDevDependencies = false;
let currentSection: 'prod' | 'dev' | null = null;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
const section = trimmed.slice(1, -1).toLowerCase();
if (section.includes('dependencies') || section.includes('requires')) {
currentSection = 'prod';
inDependencies = true;
inDevDependencies = false;
} else if (section.includes('dev-dependencies')) {
currentSection = 'dev';
inDevDependencies = true;
inDependencies = false;
} else {
inDependencies = false;
inDevDependencies = false;
currentSection = null;
}
continue;
}
if (!inDependencies && !inDevDependencies) continue;
if (trimmed.startsWith('#') || !trimmed) continue;
const match = trimmed.match(/^([a-zA-Z0-9_-]+)([<>=!~]+)(.+)$/);
if (match) {
dependencies.push({
name: match[1],
version: match[2] + match[3],
type: currentSection || 'prod',
isLocal: false,
});
} else if (/^[a-zA-Z0-9_-]+$/.test(trimmed)) {
dependencies.push({
name: trimmed,
version: '*',
type: currentSection || 'prod',
isLocal: false,
});
}
}
return dependencies;
}
private parseGoMod(content: string): Dependency[] {
const dependencies: Dependency[] = [];
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('module') || trimmed.startsWith('go')) continue;
const match = trimmed.match(/^([a-zA-Z0-9./_-]+)\s+v?([0-9.]+)/);
if (match) {
dependencies.push({
name: match[1],
version: match[2],
type: 'prod',
isLocal: match[1].startsWith('./') || match[1].startsWith('../'),
});
}
}
return dependencies;
}
private parseCargoToml(content: string): Dependency[] {
const dependencies: Dependency[] = [];
const lines = content.split('\n');
let currentSection: 'prod' | 'dev' | null = null;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
const section = trimmed.slice(1, -1).toLowerCase();
if (section === 'dependencies') {
currentSection = 'prod';
} else if (section === 'dev-dependencies') {
currentSection = 'dev';
} else {
currentSection = null;
}
continue;
}
if (!currentSection) continue;
if (trimmed.startsWith('#') || !trimmed) continue;
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*["\']?(.+?)["\']?\s*,?$/);
if (match) {
dependencies.push({
name: match[1],
version: match[2].replace(/["']/g, ''),
type: currentSection,
isLocal: false,
});
}
}
return dependencies;
}
private parsePipfile(content: string): Dependency[] {
const dependencies: Dependency[] = [];
const lines = content.split('\n');
let currentSection: 'prod' | 'dev' | null = null;
for (const line of lines) {
const trimmed = line.trim();
if (trimmed.startsWith('[') && trimmed.endsWith(']')) {
const section = trimmed.slice(1, -1).toLowerCase();
if (section === 'packages') {
currentSection = 'prod';
} else if (section === 'dev-packages') {
currentSection = 'dev';
} else {
currentSection = null;
}
continue;
}
if (!currentSection) continue;
if (trimmed.startsWith('#') || !trimmed) continue;
const match = trimmed.match(/^([a-zA-Z0-9_-]+)\s*=\s*\{?\s*version\s*=\s*["\']([^"\']+)["\']/);
if (match) {
dependencies.push({
name: match[1],
version: match[2],
type: currentSection,
isLocal: false,
});
} else {
const simpleMatch = trimmed.match(/^([a-zA-Z0-9_-]+)/);
if (simpleMatch) {
dependencies.push({
name: simpleMatch[1],
version: '*',
type: currentSection,
isLocal: false,
});
}
}
}
return dependencies;
}
private parseGemfile(content: string): Dependency[] {
const dependencies: Dependency[] = [];
const lines = content.split('\n');
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const match = trimmed.match(/^gem\s+["']([^"']+)["'](?:\s*,\s*version:\s*["']([^"']+)["'])?/);
if (match) {
dependencies.push({
name: match[1],
version: match[2] || '*',
type: 'prod',
isLocal: false,
});
}
}
return dependencies;
}
private parseComposerJson(content: string): Dependency[] {
const composerJson = JSON.parse(content);
const dependencies: Dependency[] = [];
const parseDeps = (deps: Record<string, string> | undefined, type: 'prod' | 'dev') => {
if (!deps) return;
for (const [name, version] of Object.entries(deps)) {
dependencies.push({
name,
version: version as string,
type,
isLocal: false,
});
}
};
parseDeps(composerJson.require, 'prod');
parseDeps(composerJson['require-dev'], 'dev');
return dependencies;
}
private async fileExists(filePath: string): Promise<boolean> {
try {
await fs.promises.access(filePath, fs.constants.F_OK);
return true;
} catch {
return false;
}
}
}