Read and write lockfiles with reasonable losses
MIT License
Read and write lockfiles with reasonable losses
The package.json
manifest contains its own deps requirements, the lockfile
holds the deps resolution snapshot*,
so both of them are required to build a dependency graph. We can try to convert this data into a normalized representation for further analysis and processing (for example, to fix vulnerabilities).
And then, if necessary, try convert it back to the original/another format.
Proof of concept. The API may change significantly ⚠️
yarn add @antongolub/lockfile@snapshot
tl;dr
import fs from 'fs/promises'
import {parse, analyze} from '@antongolub/lockfile'
const lf = await fs.readFile('yarn.lock', 'utf-8')
const pkg = await fs.readFile('package.json', 'utf-8')
const snapshot = parse(lf, pkg) // Holds JSON-friendly TEntries[]
const idx = analyze(snapshot) // An index to represent repo dep graphs
// idx.entries
// idx.prod
// idx.edges
import { parse, format, analyze, convert } from '@antongolub/lockfile'
const lf = await fs.readFile('yarn.lock', 'utf-8')
const pkgJson = await fs.readFile('package.json', 'utf-8')
const snapshot = parse(lf, pkgJson)
const lf1 = format(snapshot)
const lf2 = format(snapshot, 'npm-1') // Throws err: npm v1 meta does not support workspaces
const meta = await readMeta() // reads local package.jsons data to gather required data like `engines`, `license`, `bins`, etc
const meta2 = await fetchMeta(snapshot) // does the same, but from the remote registry
const lf3 = format(snapshot, 'npm-3', {meta}) // format with options
const idx = analyze(snapshot)
idx.edges
// [
// [ '', '@antongolub/[email protected]' ],
// [ '@antongolub/[email protected]', '@antongolub/[email protected]' ],
// [ '@antongolub/[email protected]', '@antongolub/[email protected]' ],
// [ '@antongolub/[email protected]', '@antongolub/[email protected]' ]
// ]
const lf4 = await convert(lf, pkgJson, 'yarn-berry')
npx @antongolub/lockfile@snapshot <cmd> [options]
npx @antongolub/lockfile@snapshot parse --input=yarn.lock,package.json --output=snapshot.json
npx @antongolub/lockfile@snapshot format --input=snapshot.json --output=yarn.lock
Command / Option | Description |
---|---|
parse |
Parses lockfiles and package manifests into a snapshot |
format |
Formats a snapshot into a lockfile |
convert |
Converts a lockfile into another format. Shortcut for parse + format
|
--input |
A comma-separated list of files to parse: snapshot.json or yarn.lock,package.json
|
--output |
A file to write the result to: snapshot.json or yarn.lock
|
--format |
A lockfile format: npm-1 , npm-2 , npm-3 , yarn-berry , yarn-classic
|
nmtree
— fs projection of deps, directories structure
deptree
— bounds full dep paths with their resolved packages
depgraph
— describes how resolved pkgs are related with each other
Package manager | Meta format | Read | Write |
---|---|---|---|
npm <7 | 1 | ✓ | ✓ |
npm >=7 | 2 | ✓ | |
npm >=9 | 3 | ✓ | |
yarn 1 (classic) | 1 | ✓ | ✓ |
yarn 2, 3, 4 (berry) | 5, 6, 7 | ✓ | ✓ |
Type | Supported | Example | Description |
---|---|---|---|
semver | ✓ | ^1.2.3 |
Resolves from the default registry |
tag | latest |
Resolves from the default registry | |
npm | ✓ | npm:name@... |
Resolves from the npm registry |
git | [email protected]:foo/bar.git |
Downloads a public package from a Git repository | |
github | github:foo/bar |
Downloads a public package from GitHub | |
github | ✓ | foo/bar |
Alias for the github: protocol |
file | file:./my-package |
Copies the target location into the cache | |
link | link:./my-folder |
Creates a link to the ./my-folder folder (ignore dependencies) | |
patch | limited | patch:[email protected]#./my-patch.patch |
Creates a patched copy of the original package |
portal | portal:./my-folder |
Creates a link to the ./my-folder folder (follow dependencies) | |
workspace | limited | workspace:* |
Creates a link to a package in another workspace |
https://v3.yarnpkg.com/features/protocols https://yarnpkg.com/protocols https://docs.npmjs.com/cli/v10/configuring-npm/package-json#dependencies
TSnapshot
export type TSnapshot = Record<string, TEntry>
export type TEntry = {
name: string
version: string
ranges: string[]
hashes: {
sha512?: string
sha256?: string
sha1?: string
checksum?: string
md5?: string
}
source: {
type: TSourceType // npm, workspace, gh, patch, etc
id: string
registry?: string
}
// optional pm-specific lockfile meta
manifest?: TManifest
conditions?: string
dependencies?: TDependencies
dependenciesMeta?: TDependenciesMeta
devDependencies?: TDependencies
optionalDependencies?: TDependencies
peerDependencies?: TDependencies
peerDependenciesMeta?: TDependenciesMeta
bin?: Record<string, string>
engines?: Record<string, string>
funding?: Record<string, string>
}
TSnapshotIndex
export interface TSnapshotIndex {
snapshot: TSnapshot
entries: TEntry[]
roots: TEntry[]
edges: [string, string][]
tree: Record<string, {
key: string
chunks: string[]
parents: TEntry[]
id: string
name: string
version: string
entry: TEntry
depth: number // the lowest level where the dep@ver first time occurs
}>
prod: Set<TEntry>
getEntryId ({name, version}: TEntry): string
getEntry (name: string, version?: string): TEntry | undefined,
getEntryByRange (name: string, range: string): TEntry | undefined
getEntryDeps(entry: TEntry): TEntry[]
}
nmtrees
that corresponds to the specified deptree
, but among them there is a finite set of effective (sufficient) for the target criterion — for example, nesting, size, homogeneity of versionsoptional: true
label is not supported yetengines
and funding
data, while yarn* or npm1 does not contain itnmtree
projections may correspond to the specified depgraph
resolutions
and overrides
directives are completely ignored for nowExtracts all deps by depth:
const getDepsByDepth = (idx: TSnapshotIndex, depth = 0) => Object.values(idx.tree)
.filter(({depth: d}) => d === depth)
.map(({entry}) => entry)
Get the longest dep chain:
const getLongestChain = (): TEntry[] => {
let max = 0
let chain: TEntry[] = []
for (const e of Object.values(idx.tree)) {
if (e.depth > max) {
max = e.depth
chain = [...e.parents, e.entry]
}
}
return chain
}
constole.log(
getLongestChain()
.map((e) => idx.getEntryId(e))
.join(' -> ')
)