Universal License Notice (uln) is a CLI for discovering package manager manifests and generating third-party license notice output from package metadata.
Its goal is to provide one tool for generating license notices across multiple package managers, saving you from having to use different tools for each manager.
Supported package managers include npm and Composer, with additional adapters planned over time.
The project is currently experimental, but available on npm for early use and feedback.
- Supported package managers:
composernpm
- Detected by
ulnbut not yet supported:pypi
Current scope:
- scans project root only
- metadata-based notice generation, no network lookups yet
- bundles local dependency license files when available
This tool is best-effort and is not legal advice.
uln requires Node.js 20 or newer.
Install from npm:
npm install -g universal-license-notice
uln --helpFor local development:
npm install
npm run build
node dist/index.js --help
npm link # If you want to directly call the binary as `uln`Reports which package managers were discovered in the current project root and whether they are currently supported.
uln scanExample output:
Project root: /path/to/project
Package manager | Support | Manifest files
----------------+-------------+--------------------------------
composer | supported | composer.json, composer.lock
npm | supported | package.json, package-lock.json
Generates third-party notice output for supported package managers in the current project root.
uln generate- Text output defaults to
THIRD_PARTY_NOTICES.txt - JSON output defaults to
NOTICE.json - HTML output defaults to
NOTICE.html - If you do not specify a format, HTML output is used by default.
- Full dependency license text is included by default when local package license files are available.
- Use
--dont-include-license-textto disable license text bundling. - Use
--exclude-devto exclude development dependencies (for managers that can identify them). - Use
--hide-fields version,homepageto hide specific dependency fields for this run.
Examples:
uln generate
uln generate --format text
uln generate --output notices.txt
uln generate --format html --output third-party-notices.html
uln generate --stdout
uln generate --stdout --format json
uln generate --exclude-dev
uln generate --format html --hide-fields version,homepage,repository
uln generate --config uln.config.json--stdout and --output cannot be used together.
If uln.config.json exists in the project root, uln generate loads it automatically.
You can also point to a config file explicitly:
uln generate --config path/to/uln.config.jsonSupported fields:
{
"managers": {
"npm": {
"excludePackages": ["example-package", "@author/*"],
"packageOverrides": {
"chalk": {
"licenseExpression": "MIT",
"repository": "https://github.com/chalk/chalk"
},
"@scope/*": {
"licenseExpression": "MIT"
}
}
},
"composer": {
"excludePackages": ["symfony/*"],
"packageOverrides": {
"monolog/monolog": {
"licenseExpression": "MIT"
}
}
}
}
}managers.<manager>.excludePackages: removes named packages from generated output;*is supported, for example@ckeditor/*managers.<manager>.packageOverrides.<name>.exclude: excludes a specific packagemanagers.<manager>.packageOverrides.<name>.licenseExpression: replaces detected license metadatamanagers.<manager>.packageOverrides.<name>.homepage,repository,author: replace detected package metadata- exact override keys win over wildcard keys when both match the same package
output.hideFields: globally hide dependency fields from rendered outputoutput.html.title: overrides the default HTML page titleoutput.html.description: overrides the default HTML page description and supports inline HTML tagsoutput.html.templatePath: path to a custom EJS template file (relative to the config file location when not absolute)output.<format>.hideFields: hide fields forhtml,text, orjsonoutput specifically--hide-fields: CLI override for hide fields (comma-separated), takes precedence over config for that command
Supported hideFields values:
versionhomepagerepositoryauthordirectlicenseExpressionlicenseTextlicenseSourcePath
The text renderer includes:
- package manager
- package name and version
- whether the dependency is direct
- license expression when available
- repository, homepage, and author when available
- bundled license file path and license text when available
- warnings for missing or non-normalized license metadata
The JSON renderer returns normalized dependency records and warnings suitable for later tooling or CI integration.
When license text bundling is enabled (default), dependency records also include licenseText and licenseSourcePath when available.
The HTML renderer outputs a clean, GitHub Pages-style notice page with collapsible dependency sections.
Both the default template and custom templates are rendered with EJS.
- each dependency is rendered as a section labeled
package@version (SPDX code) - expanded sections include author, homepage, repository, direct dependency status, and package manager information
- full license text is rendered inside
<pre>when available - description content is rendered as HTML in the default template
You can configure HTML output metadata in uln.config.json. All of these settings are optional:
{
"output": {
"html": {
"title": "Third-Party Notices",
"description": "Generated using Universal License Notice from discovered package metadata.",
"templatePath": "templates/custom-notice.ejs",
"hideFields": ["homepage"]
},
"hideFields": ["repository"],
"text": {
"hideFields": ["author"]
},
"json": {
"hideFields": ["licenseText", "licenseSourcePath"]
}
}
}The current npm adapter:
- detects
package.jsonandpackage-lock.json - prefers
package-lock.jsonfor resolved dependency metadata - uses
package.jsonand lockfile metadata to identify direct dependencies - supports modern npm lockfiles first (
lockfileVersion2 and 3) - emits warnings when license metadata is missing or could not be normalized cleanly
If package.json exists without package-lock.json, generation falls back to direct dependencies with incomplete metadata and emits warnings.
The current Composer adapter:
- detects
composer.jsonandcomposer.lock - reads dependencies from both
packagesandpackages-devincomposer.lock - marks direct dependencies using
requireandrequire-devfromcomposer.json - respects Composer
config.vendor-dirwhen searching installed package license files - emits warnings when license metadata is missing or could not be normalized cleanly
If composer.json exists without composer.lock, generation falls back to direct dependencies with incomplete metadata and emits warnings.
The current normalization layer handles a few common cases:
- common variants like
Apache License 2.0->Apache-2.0 - array license values joined as
OR - slash-delimited values like
MIT/Apache 2.0 - file-reference values like
SEE LICENSE IN LICENSE.md, with warnings
Normalization is intentionally conservative. If a value cannot be normalized confidently, it is preserved and surfaced with a warning.
Useful commands:
npm test
npm run lint
npm run buildPlanned next steps:
- PyPI adapter
- monorepo and manually provided manifest-path support
- expand configuration support beyond excludes and per-package overrides
- better output summaries and integration tests
MIT