Skip to content

feat: add support for pnpm #182

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

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
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
54 changes: 27 additions & 27 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
{
"root": true,
"extends": [
"eslint:recommended",
"plugin:editorconfig/all"
],
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"env": {
"node": true,
"mocha": true,
"es6": true
},
"plugins": [
"editorconfig"
],
"rules": {
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"sort-imports":"warn"
},
"ignorePatterns": [
"integration"
]
}
"root": true,
"extends": [
"eslint:recommended",
"plugin:editorconfig/all"
],
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "module"
},
"env": {
"node": true,
"mocha": true,
"es6": true
},
"plugins": [
"editorconfig"
],
"rules": {
"curly": "warn",
"eqeqeq": "warn",
"no-throw-literal": "warn",
"sort-imports": "warn"
},
"ignorePatterns": [
"integration"
]
}
3 changes: 3 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ jobs:
with:
go-version: '1.20.1'

- name: Install pnpm
run: npm install -g pnpm

- name: Setup Gradle
uses: gradle/actions/setup-gradle@v4

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ $ exhort-javascript-api component /path/to/pom.xml
<ul>
<li><a href="https://www.java.com/">Java</a> - <a href="https://maven.apache.org/">Maven</a></li>
<li><a href="https://www.javascript.com/">JavaScript</a> - <a href="https://www.npmjs.com/">Npm</a></li>
<li><a href="https://www.javascript.com/">JavaScript</a> - <a href="https://pnpm.io/">pnpm</a></li>
<li><a href="https://go.dev/">Golang</a> - <a href="https://go.dev/blog/using-go-modules/">Go Modules</a></li>
<li><a href="https://www.python.org/">Python</a> - <a href="https://pypi.org/project/pip/">pip Installer</a></li>
<li><a href="https://gradle.org/">Gradle (Groovy and Kotlin DSL)</a> - <a href="https://gradle.org/install/">Gradle Installation</a></li>
Expand Down Expand Up @@ -298,6 +299,7 @@ import fs from 'node:fs'
let options = {
'EXHORT_MVN_PATH': '/path/to/my/mvn',
'EXHORT_NPM_PATH': '/path/to/npm',
'EXHORT_PNPM_PATH': '/path/to/pnpm',
'EXHORT_GO_PATH': '/path/to/go',
//python - python3, pip3 take precedence if python version > 3 installed
'EXHORT_PYTHON3_PATH' : '/path/to/python3',
Expand Down Expand Up @@ -343,6 +345,11 @@ following keys for setting custom paths for the said executables.
<td>EXHORT_NPM_PATH</td>
</tr>
<tr>
<td><a href="https://pnpm.io/">PNPM</a></td>
<td><em>pnpm</em></td>
<td>EXHORT_PNPM_PATH</td>
</tr>
<tr>
<td><a href="https://go.dev/blog/using-go-modules/">Go Modules</a></td>
<td><em>go</em></td>
<td>EXHORT_GO_PATH</td>
Expand Down
23 changes: 12 additions & 11 deletions src/provider.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,21 @@
import path from 'node:path'

import golangGomodulesProvider from './providers/golang_gomodules.js'

Check warning on line 3 in src/provider.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Imports should be sorted alphabetically

Check warning on line 3 in src/provider.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Imports should be sorted alphabetically
import Java_gradle_groovy from "./providers/java_gradle_groovy.js";

Check warning on line 4 in src/provider.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Imports should be sorted alphabetically

Check warning on line 4 in src/provider.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Imports should be sorted alphabetically
import Java_gradle_kotlin from "./providers/java_gradle_kotlin.js";
import Java_maven from "./providers/java_maven.js";
import javascriptNpmProvider from './providers/javascript_npm.js'
import path from 'node:path'
import pythonPipProvider from './providers/python_pip.js'
import Javascript_npm from './providers/javascript_npm.js';

Check warning on line 8 in src/provider.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Imports should be sorted alphabetically

Check warning on line 8 in src/provider.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Imports should be sorted alphabetically
import Javascript_pnpm from './providers/javascript_pnpm.js';

/** @typedef {{ecosystem: string, contentType: string, content: string}} Provided */
/** @typedef {{isSupported: function(string): boolean, validateLockFile: function(): void, provideComponent: function(string, {}): Provided, provideStack: function(string, {}): Provided}} Provider */
/** @typedef {{isSupported: function(string): boolean, validateLockFile: function(string): void, provideComponent: function(string, {}): Provided, provideStack: function(string, {}): Provided}} Provider */

/**
* MUST include all providers here.
* @type {[Provider]}
*/
export const availableProviders = [new Java_maven(), new Java_gradle_groovy(), new Java_gradle_kotlin(), javascriptNpmProvider, golangGomodulesProvider, pythonPipProvider]
export const availableProviders = [new Java_maven(), new Java_gradle_groovy(), new Java_gradle_kotlin(), new Javascript_npm(), new Javascript_pnpm(), golangGomodulesProvider, pythonPipProvider]

/**
* Match a provider from a list or providers based on file type.
Expand All @@ -26,15 +28,14 @@
* @throws {Error} when the manifest is not supported and no provider was matched
*/
export function match(manifest, providers) {
let manifestPath = path.parse(manifest)
let provider = providers.find(prov => prov.isSupported(manifestPath.base))
if (!provider) {
const manifestPath = path.parse(manifest)
const supported = providers.filter(prov => prov.isSupported(manifestPath.base))
if (supported.length === 0) {
throw new Error(`${manifestPath.base} is not supported`)
}

if (provider) {
provider.validateLockFile(manifestPath.dir);
const provider = supported.find(prov => prov.validateLockFile(manifestPath.dir))
if(!provider) {
throw new Error(`${manifestPath.base} requires a lock file. Use your preferred package manager to generate the lock file.`);
}

return provider
}
3 changes: 2 additions & 1 deletion src/providers/base_java.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PackageURL } from 'packageurl-js'

import { invokeCommand } from "../tools.js"


Expand Down Expand Up @@ -44,7 +45,7 @@ export default class Base_Java {
let matchedScope = target.match(/:compile|:provided|:runtime|:test|:system|:import/g)
let matchedScopeSrc = src.match(/:compile|:provided|:runtime|:test|:system|:import/g)
// only add dependency to sbom if it's not with test scope or if it's root
if ((matchedScope && matchedScope[0] !== ":test" && (matchedScopeSrc && matchedScopeSrc[0] !== ":test")) || (srcDepth == 0 && matchedScope && matchedScope[0] !== ":test")) {
if ((matchedScope && matchedScope[0] !== ":test" && (matchedScopeSrc && matchedScopeSrc[0] !== ":test")) || (srcDepth === 0 && matchedScope && matchedScope[0] !== ":test")) {
sbom.addDependency(sbom.purlToComponent(from), to)
}
} else {
Expand Down
211 changes: 211 additions & 0 deletions src/providers/base_javascript.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import fs from 'node:fs'
import os from "node:os";
import path from 'node:path'
import { PackageURL } from 'packageurl-js'

Check warning on line 4 in src/providers/base_javascript.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Imports should be sorted alphabetically

Check warning on line 4 in src/providers/base_javascript.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Imports should be sorted alphabetically

import { invokeCommand } from "../tools.js";
import Sbom from '../sbom.js'

Check warning on line 7 in src/providers/base_javascript.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Imports should be sorted alphabetically

Check warning on line 7 in src/providers/base_javascript.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Imports should be sorted alphabetically

/** @typedef {import('../provider.js').Provider} */

/** @typedef {import('../provider.js').Provided} Provided */

const ecosystem = 'npm'
const defaultVersion = 'v0.0.0'

export default class Base_javascript {

/**
* @returns {string} the name of the lock file name for the specific implementation
*/
_lockFileName() {
throw new TypeError("_lockFileName must be implemented");
}

/**
* @returns {string} the command name to use for the specific JS package manager
*/
_cmdName() {
throw new TypeError("_cmdName must be implemented");
}

/**
* @returns {Array<string>}
*/
_listCmdArgs() {
throw new TypeError("_listCmdArgs must be implemented");
}

/**
* @returns {Array<string>}
*/
_updateLockFileCmdArgs() {
throw new TypeError("_updateLockFileCmdArgs must be implemented");
}

/**
* @param {string} manifestName - the subject manifest name-type
* @returns {boolean} - return true if `pom.xml` is the manifest name-type
*/
isSupported(manifestName) {
return 'package.json' === manifestName;
}

/**
* Checks if a required lock file exists in the same path as the manifest
*
* @param {string} manifestDir - The base directory where the manifest is located
* @returns {boolean} - True if the lock file exists
*/
validateLockFile(manifestDir) {
const lock = path.join(manifestDir, this._lockFileName());
return fs.existsSync(lock);
}

/**
* Provide content and content type for maven-maven stack analysis.
* @param {string} manifest - the manifest path or name
* @param {{}} [opts={}] - optional various options to pass along the application
* @returns {Provided}
*/
provideStack(manifest, opts = {}) {
return {
ecosystem,
content: this.#getSBOM(manifest, opts, true),
contentType: 'application/vnd.cyclonedx+json'
}
}

/**
* Provide content and content type for maven-maven component analysis.
* @param {string} manifest - path to pom.xml for component report
* @param {{}} [opts={}] - optional various options to pass along the application
* @returns {Provided}
*/
provideComponent(manifest, opts = {}) {
return {
ecosystem,
content: this.#getSBOM(manifest, opts, false),
contentType: 'application/vnd.cyclonedx+json'
}
}

/**
* Utility function for creating Purl String

* @param name the name of the artifact, can include a namespace(group) or not - namespace/artifactName.
* @param version the version of the artifact
* @returns {PackageURL|null} PackageUrl Object ready to be used in SBOM
*/
#toPurl(name, version) {
let parts = name.split("/");
var purlNs, purlName;
if (parts.length === 2) {
purlNs = parts[0];
purlName = parts[1];
} else {
purlName = parts[0];
}
return new PackageURL('npm', purlNs, purlName, version, undefined, undefined);
}

_buildDependencyTree(includeTransitive, manifest) {
this.#version();
let manifestDir = path.dirname(manifest)
this.#createLockFile(manifestDir);

let npmOutput = this.#executeListCmd(includeTransitive, manifestDir);
return JSON.parse(npmOutput);
}

/**
* Create SBOM json string for npm Package.
* @param {string} manifest - path for package.json
* @param {{}} [opts={}] - optional various options to pass along the application
* @returns {string} the SBOM json content
* @private
*/
#getSBOM(manifest, opts = {}, includeTransitive) {
const depsObject = this._buildDependencyTree(includeTransitive, manifest);
let rootName = depsObject["name"]
let rootVersion = depsObject["version"]
if (!rootVersion) {
rootVersion = defaultVersion
}
let mainComponent = this.#toPurl(rootName, rootVersion);

let sbom = new Sbom();
sbom.addRoot(mainComponent)

let dependencies = depsObject["dependencies"] || {};
this.#addAllDependencies(sbom, sbom.getRoot(), dependencies)
let packageJson = fs.readFileSync(manifest).toString()
let packageJsonObject = JSON.parse(packageJson);
if (packageJsonObject.exhortignore !== undefined) {
let ignoredDeps = Array.from(packageJsonObject.exhortignore);
sbom.filterIgnoredDeps(ignoredDeps)
}
return sbom.getAsJsonString(opts)
}

/**
* This function recursively build the Sbom from the JSON that npm listing returns
* @param sbom this is the sbom object
* @param from this is the current component in bom (Should start with root/main component of SBOM) for which we want to add all its dependencies.
* @param dependencies the current dependency list (initially it's the list of the root component)
* @private
*/
#addAllDependencies(sbom, from, dependencies) {
Object.entries(dependencies)
.filter(entry => entry[1].version !== undefined)
.forEach(entry => {
let [name, artifact] = entry;
let purl = this.#toPurl(name, artifact.version);
sbom.addDependency(from, purl)
let transitiveDeps = artifact.dependencies
if (transitiveDeps !== undefined) {
this.#addAllDependencies(sbom, sbom.purlToComponent(purl), transitiveDeps)
}
});
}

#executeListCmd(includeTransitive, manifestDir) {
const listArgs = this._listCmdArgs(includeTransitive, manifestDir);
try {
return invokeCommand(this._cmdName(), listArgs)
} catch (error) {
throw new Error(`failed to list dependencies via "${this._cmdName()} ${listArgs.join(' ')}" - Error: ${error}`, {cause: error});
}
}

#version() {
try {
invokeCommand(this._cmdName(), ['--version'], {stdio: 'ignore'});
} catch (error) {
if (error.code === 'ENOENT') {
throw new Error(`${this._cmdName()} is not accessible`);
}
throw new Error(`failed to check for package manager binary at ${this._cmdName()}`, {cause: error})
}
}

#createLockFile(manifestDir) {
// in windows os, --prefix flag doesn't work, it behaves really weird , instead of installing the package.json fromm the prefix folder,
// it's installing package.json (placed in current working directory of process) into prefix directory, so
let originalDir = process.cwd()
if (os.platform() === 'win32') {
process.chdir(manifestDir)
}
const args = this._updateLockFileCmdArgs(manifestDir);
try {
invokeCommand(this._cmdName(), args)
} catch (error) {
throw new Error(`failed to create lockfile "${args}" - Error: ${error}`, {cause: error});
} finally {
if (os.platform() === 'win32') {
process.chdir(originalDir)
}
}
}
}

2 changes: 1 addition & 1 deletion src/providers/golang_gomodules.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import fs from 'node:fs'
import { EOL } from "os";

Check warning on line 2 in src/providers/golang_gomodules.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Imports should be sorted alphabetically

Check warning on line 2 in src/providers/golang_gomodules.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Imports should be sorted alphabetically
import { getCustom, getCustomPath, invokeCommand } from "../tools.js";

Check warning on line 3 in src/providers/golang_gomodules.js

View workflow job for this annotation

GitHub Actions / Lint and test project (18)

Expected 'multiple' syntax before 'single' syntax

Check warning on line 3 in src/providers/golang_gomodules.js

View workflow job for this annotation

GitHub Actions / Lint and test project (latest)

Expected 'multiple' syntax before 'single' syntax
import path from 'node:path'
import Sbom from '../sbom.js'
Expand Down Expand Up @@ -32,7 +32,7 @@
/**
* @param {string} manifestDir - the directory where the manifest lies
*/
function validateLockFile() {}
function validateLockFile() { return true; }

/**
* Provide content and content type for maven-maven stack analysis.
Expand Down
6 changes: 1 addition & 5 deletions src/providers/java_gradle.js
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ export default class Java_gradle extends Base_java {
/**
* @param {string} manifestDir - the directory where the manifest lies
*/
validateLockFile() {}
validateLockFile() { return true; }

/**
* Provide content and content type for stack analysis.
Expand All @@ -131,10 +131,6 @@ export default class Java_gradle extends Base_java {
}
}

testMeNow() {
return {hello: "there"}
}

/**
* Provide content and content type for maven-maven component analysis.
* @param {string} manifest - path to pom.xml for component report
Expand Down
Loading
Loading