Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: resolve slow package checks by optimizing lookup #2

Merged
merged 4 commits into from
Feb 15, 2025
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
20 changes: 13 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ This utility provides a script to scan the dependencies and transitive dependenc

- **Configurable Mode:** Choose between warning or error modes using configuration or environment variables.

- **Reason:** Each deprecated dependency includes a reason, often suggesting alternative packages.

## Usage

You can run the utility directly from the command line once it is installed.<br>
Expand All @@ -36,12 +38,14 @@ You can change the mode in two ways:

```
Checking dependencies at root level...
1. @example/package1 DEPRECATED
1. @example/package1
Reason:

WARNING!! Deprecated results found at root level.

Checking all dependencies (including transitive)...
1. @example/package2 DEPRECATED
Checking all transitive dependencies...
1. @example/package2
Reason:

WARNING!! Deprecated results found in dependencies.
```
Expand All @@ -50,12 +54,14 @@ WARNING!! Deprecated results found in dependencies.

```
Checking dependencies at root level...
1. @example/package1 DEPRECATED
1. @example/package1
Reason:

ERROR!! Deprecated results found at root level.

Checking all dependencies (including transitive)...
1. @example/package2 DEPRECATED
Checking all transitive dependencies...
1. @example/package2
Reason:

ERROR!! Deprecated results found in dependencies.
```
Expand All @@ -66,6 +72,6 @@ ERROR!! Deprecated results found in dependencies.
Checking dependencies at root level...
SUCCESS: No deprecated packages found at root level! Congos!!

Checking all dependencies (including transitive)...
Checking all transitive dependencies...
SUCCESS: No deprecated packages found! Congos!!
```
4 changes: 2 additions & 2 deletions bin/cli.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
#!/usr/bin/env node
const { runDependencyCheck } = require('../src/index')
runDependencyCheck()
const { checkDependencies } = require('../src/index')
checkDependencies()
187 changes: 93 additions & 94 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,125 +24,124 @@
const { spawnSync } = require('child_process')
const fs = require('fs')
const path = require('path')
const https = require("https");

const configPath = path.resolve(__dirname, '../config/default.json')
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'))

const mode = process.env.MODE || config.mode

function execCommandSync (command) {
const [cmd, ...args] = command.split(' ')
const result = spawnSync(cmd, args, { encoding: 'utf-8', shell: false })

if (result.error) {
throw result.error
}
if (result.status !== 0) {
throw new Error(result.stderr || `Command failed with exit code ${result.status}`)
}
return result.stdout
function getDependencies(checkTransitive = false) {
const args = checkTransitive ? ["ls", "--all", "--json"] : ["ls", "--json"];
const output = spawnSync("npm", args, { encoding: "utf8" }).stdout;
return JSON.parse(output).dependencies || {};
}

const dependenciesMap = new Map()
const regex = /(?:@[\w-]+\/)?[\w.-]{1,100}@\d{1,10}\.\d{1,10}\.\d{1,10}(?:[-+][\w.-]{1,50})?/g

function checkDependencySync (dependency) {
if (dependenciesMap.has(dependency)) return
try {
const output = execCommandSync(`npm view ${dependency}`)
if (output.includes('DEPRECATED')) {
dependenciesMap.set(dependency, 'DEPRECATED')
} else {
dependenciesMap.set(dependency, 'active')
}
} catch (error) {
dependenciesMap.set(dependency, 'UNKNOWN')
}
async function fetchPackageMetadata(pkgName,version) {
return new Promise((resolve, reject) => {
const url = `https://registry.npmjs.org/${pkgName}/${version}`;
https.get(url, (res) => {
let data = "";

res.on("data", (chunk) => {
data += chunk;
});

res.on("end", () => {
try {
resolve(JSON.parse(data));
} catch (error) {
reject(new Error(`Failed to parse response for ${pkgName}`));
}
});
}).on("error", (error) => {
reject(new Error(`Failed to fetch metadata for ${pkgName}: ${error.message}`));
});
});
}

function processLinesSync (lines) {
for (const line of lines) {
const trimmedLine = line.trim()
const matches = trimmedLine.matchAll(regex)

for (const match of matches) {
const dependency = match[0]
checkDependencySync(dependency)
async function checkPackage(pkgName, version, level) {
try {
const packageData = await fetchPackageMetadata(pkgName,version);
const deprecatedMessage = packageData.deprecated || null;

if (deprecatedMessage) { // If a deprecation message exists
count++;
output += `${count}: ${pkgName}@${version} \n\tReason: ${deprecatedMessage}\n\n`;
}
} catch (err) {
output += `Error checking ${pkgName}@${version}: ${err.message}\n`;
}
}
}

function checkDependenciesSync (command) {
try {
const stdout = execCommandSync(command)
const lines = stdout.trim().split('\n')
processLinesSync(lines)
} catch (error) {
const errorLines = error.toString().trim().split('\n')
processLinesSync(errorLines)
}
}
function getAllPackages(deps, collected = []) {
const seen = new Set(collected.map(pkg => `${pkg.name}@${pkg.version}`)); // Track seen packages

function runDependencyCheck () {
let errorOccurred = false // Track if any errors occurred
for (const [name, info] of Object.entries(deps)) {
const version = info.version;
const packageKey = `${name}@${version}`;

console.log('Checking dependencies at root level...')
checkDependenciesSync('npm ls')
if (name && version && !seen.has(packageKey)) {
collected.push({ name, version });
seen.add(packageKey);

let deprecatedFound = false
let counter = 0
dependenciesMap.forEach((status, dependency) => {
if (status === 'DEPRECATED') {
counter++
deprecatedFound = true
console.log(`${counter}. ${dependency} ${status}`)
if (info.dependencies) getAllPackages(info.dependencies, collected);
}
}
})
return collected;
}

if (deprecatedFound) {
if (mode === 'error') {
console.error('\x1b[31mERROR!! Deprecated results found at root level.\n\x1b[0m')
errorOccurred = true // Set the error state
async function checkDependencies() {
console.log("\x1b[34mChecking root dependencies...\x1b[0m\n");
output = "";
const rootDependencies = getDependencies(false);
const rootPackageList = Object.entries(rootDependencies).map(([name, info]) => ({
name: name,
version: info.version
}));

await Promise.all(rootPackageList.map(({ name, version }) =>
checkPackage(name, version, "root")
));

if (mode === 'warning') {
output += count > 0
? '\x1b[33mWARNING!! Deprecated results found at root level.\n\x1b[0m\n'
: '\x1b[32mSUCCESS: No deprecated packages found at root level! Congos!!\n\x1b[0m\n';
} else {
console.log('\x1b[33mWARNING!! Deprecated results found at root level.\n\x1b[0m')
output += count > 0
? '\x1b[31mERROR!! Deprecated results found at root level.\n\x1b[0m\n'
: '\x1b[32mSUCCESS: No deprecated packages found at root level! Congos!!\n\x1b[0m\n';
}
} else {
console.log('\x1b[32mSUCCESS: No deprecated packages found at root level! Congos!!\n\x1b[0m')
}

console.log('Checking all dependencies (including transitive)...')
checkDependenciesSync('npm ls --all')

deprecatedFound = false
counter = 0
dependenciesMap.forEach((status, dependency) => {
if (status === 'DEPRECATED') {
counter++
deprecatedFound = true
console.log(`${counter}. ${dependency} ${status}`)
}
})

if (deprecatedFound) {
if (mode === 'error') {
console.error('\x1b[31mERROR!! Deprecated results found in dependencies.\n\x1b[0m')
errorOccurred = true // Set the error state
console.log(output);

console.log("\x1b[34m\nChecking all transitive dependencies...\x1b[0m\n");
output = "";
const allDependencies = getDependencies(true);
const allPackageList = getAllPackages(allDependencies);

await Promise.all(allPackageList.map(({ name, version }) =>
checkPackage(name, version, "transitive")
));

if (mode === 'warning') {
output += count > 0
? '\x1b[33mWARNING!! Deprecated results found in dependencies.\n\x1b[0m\n'
: '\x1b[32mSUCCESS: No deprecated packages found! Congos!!\x1b[0m\n';
} else {
console.log('\x1b[33mWARNING!! Deprecated results found in dependencies.\n\x1b[0m')
output += count > 0
? '\x1b[31mERROR!! Deprecated results found in dependencies.\n\x1b[0m\n'
: '\x1b[32mSUCCESS: No deprecated packages found! Congos!!\x1b[0m\n';
}
} else {
console.log('\x1b[32mSUCCESS: No deprecated packages found! Congos!!\x1b[0m')
}

// At the end of execution, handle the error state if needed
if (errorOccurred) {
console.error('\x1b[31mProcess completed with errors due to deprecated dependencies.\x1b[0m')
if (mode === 'error') {
process.exit(1) // Exit with an error code after finishing everything

console.log(output);

if (mode === "error" && count > 0) {
process.exit(1);
}
}
}

module.exports = {
runDependencyCheck
checkDependencies
}