ncm-cli
Version:
Command-line tool for NodeSource Certified Modules 2.0
547 lines (459 loc) • 14.8 kB
JavaScript
'use strict'
const { graphql } = require('./util')
const semver = require('semver')
const fs = require('fs')
const path = require('path')
// No need for patches since we're not using universal-module-tree anymore
// Use dependency-tree package instead of universal-module-tree
const dependencyTree = require('dependency-tree');
// Helper function to convert dependency-tree output to a format similar to universal-module-tree
const buildDependencyTree = (filename, directory) => {
// Make sure directory is absolute
const absDirectory = path.isAbsolute(directory) ? directory : path.resolve(process.cwd(), directory);
// Analyze with dependency-tree
try {
// Check if the target file exists
const targetFilePath = path.resolve(absDirectory, filename);
if (!fs.existsSync(targetFilePath)) {
// Main file doesn't exist, fall back to package.json
return { children: [] };
}
// Get the dependency tree in object form
// First attempt: analyze the application code
let tree = dependencyTree({
filename: targetFilePath,
directory: absDirectory,
filter: path => path.indexOf('node_modules') === -1, // Skip node_modules
noTypeDefinitions: true // Skip TypeScript definitions
});
// Now we need to get npm dependencies from package.json since we excluded node_modules
// This approach combines both static analysis and package.json info
const npmDeps = getNpmDependencies(absDirectory);
// Mix in the npm dependencies from package.json
// Convert to a format similar to universal-module-tree
return convertToUniversalModuleTree(tree, absDirectory);
} catch (err) {
// Error analyzing dependencies
return { children: [] };
}
};
// Helper function to get npm dependencies from package.json
function getNpmDependencies(directory) {
const deps = [];
const pkgJsonPath = path.join(directory, 'package.json');
try {
if (fs.existsSync(pkgJsonPath)) {
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
// Combine all dependency types
const allDeps = {
...pkgJson.dependencies || {},
...pkgJson.devDependencies || {},
...pkgJson.peerDependencies || {},
...pkgJson.optionalDependencies || {}
};
// Create a dependency object for each npm package
for (const [name, version] of Object.entries(allDeps)) {
// Clean up version strings (remove ^, ~, etc.)
let cleanVersion = version;
if (typeof version === 'string') {
cleanVersion = version.replace(/^[^0-9]*/, '');
}
deps.push({
name,
version: cleanVersion || '0.0.0'
});
}
}
} catch (err) {
// Error reading package.json
}
return deps;
}
// Convert dependency-tree format to universal-module-tree format
function convertToUniversalModuleTree(tree, baseDir) {
// Get the root node (first key in the object)
const rootKey = Object.keys(tree)[0];
if (!rootKey) return { children: [] };
// Extract package info from package.json if available
const pkgJsonPath = path.join(baseDir, 'package.json');
let pkgInfo = { name: path.basename(baseDir), version: '0.0.0' };
try {
if (fs.existsSync(pkgJsonPath)) {
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
pkgInfo = {
name: pkgJson.name || pkgInfo.name,
version: pkgJson.version || pkgInfo.version
};
}
} catch (err) {
// Ignore package.json errors
}
// Add npm dependencies directly to the tree
const npmDeps = getNpmDependencies(baseDir);
// Create the root node with children
const result = {
data: pkgInfo,
children: []
};
// Process all dependencies from the static analysis
function processNode(treeNode, parentNode) {
const deps = Object.keys(treeNode);
for (const dep of deps) {
// Extract name and version from the dependency path
// For simplicity, we'll use the filename as the name
const name = path.basename(dep, path.extname(dep));
// Create the child node
const childNode = {
data: {
name,
version: '0.0.0' // Default version since we don't have this info
},
children: []
};
// Process subdependencies
processNode(treeNode[dep], childNode);
// Add to parent's children
parentNode.children.push(childNode);
}
}
// Start processing from the root
if (rootKey) {
processNode(tree[rootKey], result);
}
// Add npm dependencies from package.json as direct children of the root node
for (const dep of npmDeps) {
// Add npm package as a direct child
result.children.push({
data: {
name: dep.name,
version: dep.version
},
children: []
});
}
return result;
}
const analyze = async ({
dir,
token,
pageSize = 50,
concurrency = 5,
onPkgs = () => {},
filter = () => true,
url
}) => {
// Get all dependencies and apply filter
const rawDeps = await readUniversalTree(dir);
const pkgs = filterPkgs(rawDeps, filter);
onPkgs(pkgs);
const data = new Set();
const pages = splitSet(pkgs, pageSize);
const batches = splitSet(pages, concurrency);
// Process each batch
for (const batch of batches) {
await Promise.all([...batch].map(async page => {
const fetchedData = await fetchData({ pkgs: page, token, url });
for (const datum of fetchedData) {
data.add(datum);
}
}));
}
return data
}
const filterPkgs = (pkgs, fn) => {
const map = new Map()
let validCounter = 0;
let invalidCounter = 0;
let skippedCounter = 0;
for (const pkg of pkgs) {
const id = `${pkg.name}${pkg.version}`
if (!semver.valid(pkg.version)) {
invalidCounter++;
continue;
}
if (map.get(id)) {
skippedCounter++;
continue;
}
if (fn(pkg)) {
map.set(id, pkg)
validCounter++;
} else {
skippedCounter++;
}
}
// Filtering complete
const clean = new Set()
for (const [, pkg] of map) clean.add(pkg)
return clean
}
const id = node => `${node.data.name}@${node.data.version}`
// This function is only used as a fallback now, using the getNpmDependencies function
// to directly extract package.json dependencies in our main workflow
async function readPackagesFromPackageJson(dir) {
const npmDeps = getNpmDependencies(dir);
// Convert to the same format as the tree structure
const pkgJsonPath = path.join(dir, 'package.json');
let pkgInfo = { name: path.basename(dir), version: '0.0.0' };
try {
if (fs.existsSync(pkgJsonPath)) {
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
pkgInfo = {
name: pkgJson.name || pkgInfo.name,
version: pkgJson.version || pkgInfo.version
};
}
} catch (err) {
// Ignore package.json errors
}
// Create result structure
const result = {
data: pkgInfo,
children: []
};
// Add all npm dependencies as children
for (const dep of npmDeps) {
result.children.push({
data: {
name: dep.name,
version: dep.version
},
children: []
});
}
return result;
}
const readUniversalTree = async dir => {
let treeResult;
try {
// Use our new dependency tree builder instead of universalModuleTree
// First, find the main file from package.json or use typical entry points
const pkgJsonPath = path.join(dir, 'package.json');
let mainFile = null;
let pkgJson = null;
if (fs.existsSync(pkgJsonPath)) {
try {
pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf8'));
if (pkgJson.main) {
mainFile = pkgJson.main;
} else if (pkgJson.bin) {
// If there's no main but there is a bin field, use the first bin entry
if (typeof pkgJson.bin === 'string') {
mainFile = pkgJson.bin;
} else if (typeof pkgJson.bin === 'object') {
// Use the first bin entry if it's an object
const firstBin = Object.values(pkgJson.bin)[0];
if (firstBin) {
mainFile = firstBin;
}
}
}
} catch (e) {
// Ignore package.json errors
// Error reading package.json
}
}
// Check if the main file exists, otherwise try common entry points
if (mainFile && !fs.existsSync(path.join(dir, mainFile))) {
// Main file not found, trying alternatives
mainFile = null;
}
if (!mainFile) {
// Try common entry points
const possibleEntryPoints = [
'index.js',
'app.js',
'server.js',
'main.js',
'bin/index.js',
'lib/index.js'
];
// If we have package.json info, try using the name as entry point
if (pkgJson && pkgJson.name) {
possibleEntryPoints.unshift(`bin/${pkgJson.name}.js`);
possibleEntryPoints.unshift(`${pkgJson.name}.js`);
}
for (const entryPoint of possibleEntryPoints) {
if (fs.existsSync(path.join(dir, entryPoint))) {
mainFile = entryPoint;
break;
}
}
// If still no main file found, make one last attempt with bin directory
if (!mainFile && fs.existsSync(path.join(dir, 'bin'))) {
try {
const binFiles = fs.readdirSync(path.join(dir, 'bin'));
if (binFiles.length > 0) {
// Use the first .js file in the bin directory
const jsFile = binFiles.find(file => file.endsWith('.js'));
if (jsFile) {
mainFile = `bin/${jsFile}`;
}
}
} catch (e) {
// Ignore errors reading bin directory
}
}
}
// Starting dependency analysis
// Build the dependency tree starting from the main file
treeResult = buildDependencyTree(mainFile, dir);
// We should always have dependencies from package.json now
// but fall back to the old method if something goes wrong
if (!treeResult || !treeResult.children || treeResult.children.length === 0) {
// Using fallback package detection from package.json
treeResult = await readPackagesFromPackageJson(dir);
}
} catch (err) {
// Try to find packages by reading package.json
try {
// Using fallback package detection from package.json
treeResult = await readPackagesFromPackageJson(dir);
} catch (fallbackErr) {
// Fallback also failed
return new Set();
}
}
// At this point, we must have a valid tree from either dependency-tree or package.json
// Get packages from the tree structure
const pkgs = new Map()
const walk = (node, path) => {
// Check if node is valid
if (!node || !node.data) return;
let pkgObj
if (pkgs.has(id(node))) {
pkgObj = pkgs.get(id(node))
pkgObj.paths.push(path)
} else {
pkgObj = {
name: node.data.name,
version: node.data.version,
paths: [path]
}
pkgs.set(id(node), pkgObj)
for (const child of (node.children || [])) {
walk(child, [...path, node])
}
}
}
// Start walking from the tree structure
if (treeResult instanceof Set) {
// Direct Set result from readPackagesFromPackageJson
return treeResult;
}
// Now we know treeResult is an object, not a Set
const treeObj = treeResult;
if (treeObj && treeObj.data) {
// Single root node case
walk(treeObj, [])
} else if (treeObj && treeObj.children && Array.isArray(treeObj.children)) {
// Multiple children case
for (const child of treeObj.children) {
if (child && child.data) {
walk(child, [])
}
}
}
const set = new Set()
for (const [, pkg] of pkgs) set.add(pkg)
return set
}
const fetchData = async ({ pkgs, token, url }) => {
const query = `
query getPackageVersions($packageVersions: [PackageVersionInput!]!) {
packageVersions(packageVersions: $packageVersions) {
name
version
published
publishedAt
scores {
group
name
pass
severity
title
data
}
}
}
`
const variables = {
packageVersions: [...pkgs].map(({ name, version }) => ({ name, version }))
}
const res = await graphql(url, query, variables)
const data = new Set()
for (const datum of res.packageVersions) {
// datum.paths = [...pkgs][i].paths
data.add(datum)
}
// Packages were evaluated by NCM service
return data
}
const splitSet = (set, n) => {
const buckets = new Set()
let bucket
for (const member of set) {
if (!bucket) bucket = new Set()
bucket.add(member)
if (bucket.size === n) {
buckets.add(bucket)
bucket = null
}
}
if (bucket) buckets.add(bucket)
return buckets
}
// Function to read packages from package.json
async function readPackagesFromPackageJson(dir) {
const packageJsonPath = path.join(dir, 'package.json');
// Check if package.json exists
if (!fs.existsSync(packageJsonPath)) {
// No package.json found
return new Set();
}
// Read and parse package.json
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
const result = new Set();
// Add the main package
if (packageJson.name && packageJson.version) {
result.add({
name: packageJson.name,
version: packageJson.version
});
}
// Add dependencies
if (packageJson.dependencies) {
for (const [name, version] of Object.entries(packageJson.dependencies)) {
// Clean up the version string (remove ^, ~, etc.)
const cleanVersion = version.replace(/[^\d.]/g, '') || version;
result.add({
name,
version: cleanVersion
});
}
}
// Add devDependencies
if (packageJson.devDependencies) {
for (const [name, version] of Object.entries(packageJson.devDependencies)) {
// Clean up the version string
const cleanVersion = version.replace(/[^\d.]/g, '') || version;
result.add({
name,
version: cleanVersion
});
}
}
// Add peerDependencies
if (packageJson.peerDependencies) {
for (const [name, version] of Object.entries(packageJson.peerDependencies)) {
// Clean up the version string
const cleanVersion = version.replace(/[^\d.]/g, '') || version;
result.add({
name,
version: cleanVersion
});
}
}
return result;
}
module.exports = analyze