Collects license information of all third-party dependencies of your module and prints it to a nice html document, and optionally to a CycloneDX SBOM.
npm install --save "@volumegraphics/license-info-printer"
Requires Node.js 20.18.0 or newer.
An example can be found here: @volumegraphics/license-info-printer-example
You can run the license-info-printer command from the node_modules/.bin directory.
The CLI can be integrated into your product build chain. The process will return an error code and error message if it fails and interrupt your build process.
The following arguments are available to the license-info-printer CLI:
| Argument | Required | Description |
|---|---|---|
| productPackageJsonFile | X | File path to your package.json file of your product. The dependencies, devDependencies and optionalDependencies fields in your package.json are all considered to be valid dependencies of your product |
| productNodeModulesPaths | X | Directory paths to all node_modules folder that your product depends on. Multiple – see Multiple values section. |
| downloadCmd | A commandline that will be executed before license generation to download files for license generation. The placeholder <downloadDir> defines the directory where the command should download its files | |
| licenseFilesPath | X | Directory folder path containing all your license files. The path can be prefixed with <download> to access downloaded files (see downloadCmd option). |
| configFilePath | X | File path location of the config file. It is used to validate licenses, complete missing license information or overwrite incorrect license information of some modules. See config.json Structure section. The path can be prefixed with <download> to access downloaded files (see downloadCmd option). |
| handlebarsTemplate | X | A document template file based on "handlebars" template engine that is used to print your license file. See Document template section. Multiple – pass one template per output document; the n-th handlebarsTemplate is rendered into the n-th documentFile. Each path can be prefixed with <download> to access downloaded files (see downloadCmd option). |
| documentFile | X | File path location to a generated document. Multiple – must be given exactly as many values as handlebarsTemplate, and the values must be distinct. See Generating several documents in one run section. |
| sbomFile | File path location to a generated CycloneDX JSON SBOM. Multiple – pass one value per SBOM. Each lists every package the collector found, unless sbomPackageList gives it a package list. See CycloneDX SBOM section. Either this or a handlebarsTemplate/documentFile pair is required. |
|
| sbomPackageList | Makes one SBOM list exactly the packages named in a file instead of every collected package, given as <sbomFile>=<listFile>. The list file is a JSON array of "name@version" strings, such as a bundler plugin can write for the packages a build ships. Multiple – one value per SBOM that gets a list. See What ships section. |
|
| sbomSpecVersion | CycloneDX specification version of every sbomFile. Allowed values are "1.6", "1.5" and "1.4". Default is "1.6". |
|
| errorLogFile | File path location to the error log file. | |
| disableNpmVersionCheck | By default, the license printer insits on a correct npm license string (see spdx for more information). If it is incorrect, it will give you an error. If you set the "disableNpmVersionCheck" flag, it will not do this. | |
| errorLevelRedundantHomepageOverrides | Allowed values are "error" and "suppress". Default is "error". if "error" is set, the license printer will give you an error if you have put a hompage override for a license in your config.json but this license is not used by your product. If you set it to "suppress", nothing will happen. | |
| errorLevelRedundantLicenseOverrides | Allowed values are "error" and "suppress". Default is "error". if "error" is set, the license printer will give you an error if you have put a license override in your config.json but this license is not used by your product. If you set it to "suppress", nothing will happen. |
Console printings will notify you if an error occured.
Arguments marked Multiple accept more than one value. Write the values separated by spaces after the argument, or repeat the argument:
--productNodeModulesPaths ./node_modules ../shared/node_modules
--productNodeModulesPaths ./node_modules --productNodeModulesPaths ../shared/node_modules
Both forms are equivalent. A value that starts with - has to use the --argument=value form.
The dependency tree is collected and validated once, and every handlebarsTemplate is rendered from that same data. So instead of running the printer several times, you can produce a license page, a summary and a CycloneDX SBOM in a single run:
license-info-printer \
--productPackageJsonFile package.json \
--productNodeModulesPaths ./node_modules \
--licenseFilesPath ./license_files \
--configFilePath config.json \
--handlebarsTemplate template.html summary.hbs \
--documentFile licenses.html summary.txt \
--sbomFile my-product-1.2.3-sbom.json \
--errorLogFile license_error.txt
All outputs are produced before the first file is written, so a broken template or an invalid SBOM leaves no output files behind at all. Note that files from an earlier successful run are not deleted when a run fails, unlike the errorLogFile.
For a CycloneDX SBOM use --sbomFile rather than a template: see CycloneDX SBOM.
Instead of using the Command Line Interface, you can invoke the license-info-printer from your code.
sbomDependencyTypes and the dependencyTypes of an sboms entry are gone, and so are the DEPENDENCY_TYPES and DependencyType exports: dependency fields cannot tell what a bundled product ships. An SBOM lists every package the collector found again, as before 8.0.0, or, with sbomPackageList or packages, exactly the packages a bundler recorded. To migrate, drop the option, and for an SBOM of what ships see What ships.
Without sbomDependencyTypes, an SBOM now lists what your product reaches through its dependencies, devDependencies and optionalDependencies instead of every package the collector found. That leaves out package.json files nothing needs, such as test fixtures and benchmarks inside other packages, which the collector keeps because another package names them, for example in its own devDependencies. The handlebars documents are unchanged.
The sbom: SbomOptions option became sboms: SbomOptions[], and DocumentResult.sbom: string became sboms: string[] (one entry per requested SBOM, in the same order), so that one run can produce several SBOMs. To migrate, wrap your SBOM options in an array and read sboms[0]. The Command Line Interface is unchanged for single SBOM invocations.
toDocument now takes a single options object instead of positional arguments, handlebarsTemplate: string became handlebarsTemplates: string[], and DocumentResult.document: string became documents: string[] (one entry per template, in the same order). To migrate, name your arguments, wrap your template in an array and read documents[0]. The Command Line Interface is unchanged for single document invocations.
Example:
import * as lip from "@volumegraphics/license-info-printer";
... // see CLI section for what the options mean.
const doc = await lip.toDocument({
productPackageJsonFile,
productNodeModulesPaths, // array type
downloadCmd, // optional
licenseFilesPath,
configFilePath,
handlebarsTemplates, // array type, optional
disableNpmVersionCheck, // optional, default false
errorLevel: { // optional, both default to "error"
redundantHomepageOverrides,
redundantLicenseOverrides
},
excludeMissingPackages, // array type, optional
sboms: [{ // one entry per SBOM, optional
specVersion: "1.6", // optional, default "1.6"
includeLicenseText: false // optional, default false
}, {
packages: ["react@17.0.2"] // optional: exactly these collected packages instead of everything
}]
});
if (doc.type === "Error") {
for(let m of doc.message) {
console.log(m);
}
return;
}
// one entry per template, in the same order
doc.documents.forEach(document => console.log(document));
// one entry per entry of the sboms option, in the same order
doc.sboms.forEach(sbom => console.log(sbom));You can use the config.json from the Example as template.
{
"licenses" : [ // set of allowed licenses
{
"name": "<Name of a valid NPM license>",
"file": "<Path to license file>"
}
],
"overrides" : { // overrides invalid licenses information of third-party modules
"homepage" : [ // if "homepage" or "author" is not set, you need to overwrite it here.
{
"name": "<Exact Name of library>",
"version": "<Version string>",
"new" : "<Actual hompage / author>"
}
],
"license" : [ // if "license" is incorrect, you need to overwrite it here
{
"name": "<Name of the library>",
"version": "<Version string>",
"new": "<Valid new license>",
"comment": "<fyi>"
}
],
"purl" : [ // only used when an sbomFile is generated. See **CycloneDX SBOM** section.
{
"name": "<Name of the library>",
"version": "<Version string>",
"new": "<Complete package URL, or null for no package URL at all>",
"comment": "<fyi>"
}
]
}
}Replaces the package URL of one library in the SBOM. Matched on an exact name and version, like the other overrides; an override matching no installed library is reported as a warning rather than an error, so a stale version is visible instead of silently doing nothing. Without it, an npm package URL is built from the name and version, which is wrong for a library that reaches your product from another ecosystem or is named differently there. The value in new is used verbatim, so it has to be a complete purl - nothing checks its syntax, and the purl is the field Black Duck matches on. Use null for a library that no package URL names correctly: none is emitted, which is better than a wrong one.
This affects the SBOM only. The rendered handlebars documents are unchanged by it.
The documents Template uses the template engine "handlebars". See https://handlebarsjs.com/ on how to configure it.
If you pass several templates, every one of them receives the same data, and index restarts at 1 in each document.
Data layout for the handlebars template:
{
licenses: [
{
index: "<Array index of license>",
name: "<Name of license>",
licenseText: "<The license text>",
libraries: [
name: "<Library name>",
version: "<Library version>",
copyright: "<Copyright holder of the library>"
]
}
]
}You can use the template.html file from the Example as template.
When your template produces JSON (e.g. an SBOM / CycloneDX document), values such as an author or copyright holder may contain characters that are not valid inside a JSON string, for example the double quotes in "BB" Bob Bingo. Handlebars' default {{ }} output performs HTML escaping (which is invalid for JSON), and {{{ }}} performs no escaping at all, so both produce broken JSON.
Two helpers are registered to encode any value correctly using JSON.stringify (handling quotes, backslashes, newlines, control characters and unicode). If what you want is a CycloneDX SBOM, prefer the built-in sbomFile option over a template: a template can produce any text, so nothing checks that the result is a valid SBOM. See CycloneDX SBOM.
| Helper | Renders | Use it like |
|---|---|---|
{{json value}} |
A complete JSON literal, including the surrounding quotes. | "author": {{json copyright}} |
{{jsonEscape value}} |
Only the escaped string contents, without surrounding quotes, so you keep your own. | "author": "{{jsonEscape copyright}}" |
A missing value (undefined) is rendered as an empty string. Example SBOM template snippet:
--sbomFile writes a CycloneDX JSON SBOM from the same dependency data as the handlebars documents, so one run can produce both:
license-info-printer \
--productPackageJsonFile package.json \
--productNodeModulesPaths ./node_modules \
--licenseFilesPath ./license_files \
--configFilePath config.json \
--handlebarsTemplate template.html \
--documentFile licenses.html \
--sbomFile my-product-1.2.3-sbom.json \
--errorLogFile license_error.txt
Unlike a handlebars template, the SBOM is generated by the CycloneDX library and validated against the CycloneDX JSON schema before anything is written. If the collected data is unusable or the resulting document does not validate, the run fails with an error message and no files are written at all, not even the handlebars documents. Validation is never skipped: if the validator itself is unavailable, that is an error too.
Emitted per component: type, name, group for scoped packages, version, a purl such as pkg:npm/%40scope/name@1.2.3, a unique bom-ref, the licenses entry, and the homepage as an external reference. The document carries a serialNumber, metadata.timestamp, metadata.tools, metadata.component for your product, and a dependencies graph.
Licenses follow the CycloneDX convention: a valid SPDX identifier is written as license.id, a valid SPDX expression as expression, and anything else as free text in license.name. So a config.json entry like "Public Domain", which is not an SPDX identifier, still produces a valid document.
Two notes on the contents:
- Without a package list the SBOM lists every package the collector found, while the handlebars documents only show licenses that have a license text file configured in your
config.json. The two outputs can therefore legitimately differ. - Only the product's own
devDependenciesappear asdependenciesedges. npm never installs the devDependencies of a dependency, so such an edge could only be resolved against some unrelated package that happens to satisfy the version range. serialNumberandmetadata.timestampdiffer on every run, so the file is not byte-for-byte reproducible even though all lists are sorted.
Pass several sbomFile values to write several SBOMs from the same collected dependency tree, and give some of them a package list with sbomPackageList – for example everything that was built, and what ships: see What ships.
An SBOM without a package list lists every package the collector found: what your product needs to build and to run, and also installed package.json files that nothing in it needs, such as test fixtures inside other packages.
Dependency fields describe what your package.json declares, which is not what a bundled product ships: a loader listed under dependencies contributes no code to the bundle, while style-loader, usually a devDependency, injects runtime code into it. For an SBOM of what actually ships, let your bundler record which packages have code in its output - a JSON array of "name@version" strings, for example written by a webpack plugin when the build is done - and hand that list to sbomPackageList:
license-info-printer \
--productPackageJsonFile package.json \
--productNodeModulesPaths ./node_modules \
--licenseFilesPath ./license_files \
--configFilePath config.json \
--sbomFile my-product-1.2.3-all-sbom.json my-product-1.2.3-sbom.json \
--sbomPackageList my-product-1.2.3-sbom.json=node_modules/.cache/bundled-packages.json \
--errorLogFile license_error.txt
The SBOM then lists exactly those of the collected packages, and its dependencies graph the edges between them. A listed package the collector did not find, such as one outside productNodeModulesPaths, is reported as a warning.
The default sbomSpecVersion of 1.6 needs Black Duck 2025.1.0 or newer. Pass --sbomSpecVersion 1.4 for older installations. Upload the file under Scans → Upload File as type SBOM-CycloneDX, then map the scan to a project and version.
Be aware of how Black Duck treats the document:
- Component licenses are not persisted for components Black Duck matches against its own KnowledgeBase; it only reads them when auto-creating components it cannot match. Use the handlebars document, not the SBOM, as your license attribution artifact.
- A component without a
namemakes Black Duck reject the whole SBOM. That is why generation fails if any package is missing one. purlis what Black Duck matches on, and auto-creation of unmatched components requires it.- Uploading a file whose name matches an earlier upload overwrites it, so put your product name and version in the file name.
dependenciesandscopeare ignored on import, so build-time-only packages cannot be flagged as such: every component is imported as a prerequisite. Upload an SBOM of what ships instead, see What ships.
If you want to have more control over your license evaluations, have a look at @volumegraphics/license-info-collector The license-info-printer uses this library under the hood.