snyk-nodejs-lockfile-parser
Version:
Generate a dep tree given a lockfile
211 lines • 9.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.getLockfileVersionFromFile = exports.NodeLockfileVersion = void 0;
exports.extractPnpmMainDocument = extractPnpmMainDocument;
exports.getPnpmLockfileVersion = getPnpmLockfileVersion;
exports.getYarnLockfileVersion = getYarnLockfileVersion;
exports.getNpmLockfileVersion = getNpmLockfileVersion;
exports.parseJsonFile = parseJsonFile;
exports.describeLikelyJsonCause = describeLikelyJsonCause;
exports.loadYamlOrNull = loadYamlOrNull;
exports.loadYamlMappingOrThrow = loadYamlMappingOrThrow;
const fs_1 = require("fs");
const js_yaml_1 = require("js-yaml");
const errors_1 = require("./errors");
const error_catalog_nodejs_public_1 = require("@snyk/error-catalog-nodejs-public");
var NodeLockfileVersion;
(function (NodeLockfileVersion) {
NodeLockfileVersion["NpmLockV1"] = "NPM_LOCK_V1";
NodeLockfileVersion["NpmLockV2"] = "NPM_LOCK_V2";
NodeLockfileVersion["NpmLockV3"] = "NPM_LOCK_V3";
NodeLockfileVersion["YarnLockV1"] = "YARN_LOCK_V1";
NodeLockfileVersion["YarnLockV2"] = "YARN_LOCK_V2";
NodeLockfileVersion["PnpmLockV5"] = "PNPM_LOCK_V5";
NodeLockfileVersion["PnpmLockV6"] = "PNPM_LOCK_V6";
NodeLockfileVersion["PnpmLockV9"] = "PNPM_LOCK_V9";
})(NodeLockfileVersion || (exports.NodeLockfileVersion = NodeLockfileVersion = {}));
const getLockfileVersionFromFile = (targetFile) => {
const lockFileContents = (0, fs_1.readFileSync)(targetFile, 'utf-8');
if (targetFile.endsWith('package-lock.json')) {
return getNpmLockfileVersion(lockFileContents);
}
else if (targetFile.endsWith('yarn.lock')) {
return getYarnLockfileVersion(lockFileContents);
}
else if (targetFile.endsWith('pnpm-lock.yaml')) {
return getPnpmLockfileVersion(lockFileContents);
}
else {
throw new errors_1.InvalidUserInputError(`Unknown lockfile ${targetFile}. ` +
'Please provide either package-lock.json, yarn.lock or pnpm-lock.yaml');
}
};
exports.getLockfileVersionFromFile = getLockfileVersionFromFile;
const PNPM_YAML_DOCUMENT_START = '---\n';
const PNPM_YAML_DOCUMENT_SEPARATOR = '\n---\n';
/**
* Extract the dependency lockfile document from pnpm-lock.yaml content.
*
* Since pnpm 11, projects that use configDependencies or a pnpm-managed
* package manager version (devEngines.packageManager) get a multi-document
* pnpm-lock.yaml: an env/config document first, then the dependency
* lockfile, both marked lockfileVersion 9.0. Follows pnpm's own
* extractMainDocument() (lockfile/fs/src/yamlDocuments.ts): content that
* does not start with a document-start marker is returned unchanged, and an
* env-only file (second separator with nothing after it) yields ''. A
* leading byte-order mark is ignored for detection (pnpm's readers
* strip-bom before the marker check) but preserved on the no-marker path.
* One deviation: pnpm's writer always emits both separators, so content
* with a lone document-start marker and no second separator is not a pnpm
* 11 multi-document lockfile — it is returned unchanged and parses exactly
* as it did before this function existed (a bare '---\n' is still rejected
* loudly; an explicit-start single document still parses as one document).
*/
function extractPnpmMainDocument(content) {
const normalized = content.replace(/\r\n/g, '\n');
const detectable = normalized.charCodeAt(0) === 0xfeff ? normalized.slice(1) : normalized;
if (!detectable.startsWith(PNPM_YAML_DOCUMENT_START)) {
return normalized;
}
const sep = detectable.indexOf(PNPM_YAML_DOCUMENT_SEPARATOR, PNPM_YAML_DOCUMENT_START.length);
if (sep === -1) {
return normalized;
}
return detectable.slice(sep + PNPM_YAML_DOCUMENT_SEPARATOR.length);
}
function getPnpmLockfileVersion(lockFileContents) {
const mainDocument = extractPnpmMainDocument(lockFileContents);
if (!mainDocument && lockFileContents) {
// env-only lockfile: only pnpm 11+ writes this shape
return NodeLockfileVersion.PnpmLockV9;
}
const rawPnpmLock = loadYamlMappingOrThrow(mainDocument, 'pnpm-lock.yaml');
const { lockfileVersion } = rawPnpmLock;
const version = typeof lockfileVersion === 'string' ? lockfileVersion : '';
if (version.startsWith('5')) {
return NodeLockfileVersion.PnpmLockV5;
}
else if (version.startsWith('6')) {
return NodeLockfileVersion.PnpmLockV6;
}
else if (version.startsWith('9')) {
return NodeLockfileVersion.PnpmLockV9;
}
else {
throw new error_catalog_nodejs_public_1.OpenSourceEcosystems.PnpmUnsupportedLockfileVersionError(`The pnpm-lock.yaml lockfile version ${lockfileVersion} is not supported`);
}
}
function getYarnLockfileVersion(lockFileContents) {
if (lockFileContents.includes('__metadata')) {
return NodeLockfileVersion.YarnLockV2;
}
else {
return NodeLockfileVersion.YarnLockV1;
}
}
function getNpmLockfileVersion(lockFileContents) {
// Parse first; surfacing the real JSON error happens inside parseJsonFile.
const lockfileJson = parseJsonFile(lockFileContents, 'package-lock.json');
// The version check runs *outside* the parse try/catch so that an
// unsupported (but otherwise valid JSON) lockfile is not mis-reported as a
// JSON syntax error.
const lockfileVersion = lockfileJson.lockfileVersion || null;
switch (lockfileVersion) {
case null:
case 1:
return NodeLockfileVersion.NpmLockV1;
case 2:
return NodeLockfileVersion.NpmLockV2;
case 3:
return NodeLockfileVersion.NpmLockV3;
default:
throw new errors_1.InvalidUserInputError(`Unsupported npm lockfile version "${lockfileVersion}" in package-lock.json. ` +
'Please provide a package-lock.json with lockfileVersion 1, 2 or 3');
}
}
/**
* Parse JSON from a manifest or lockfile. On failure throws an
* InvalidUserInputError that preserves the underlying parser message
* (including the position of the syntax error) and appends a best-effort hint
* about the likely cause.
*
* `fileLabel` is the file kind shown in the error, e.g. 'package.json' or
* 'package-lock.json'.
*/
function parseJsonFile(content, fileLabel) {
try {
return JSON.parse(content);
}
catch (e) {
throw new errors_1.InvalidUserInputError(`${fileLabel} parsing failed with error ${e.message}` +
describeLikelyJsonCause(content));
}
}
/**
* Best-effort, allocation-light hint describing the most likely reason a JSON
* parse failed. Inspects only the leading characters of the content, never
* throws, and returns '' when nothing recognisable is found - so it is always
* safe to append to a parse-error message.
*/
function describeLikelyJsonCause(content) {
if (!content) {
return ' The file is empty.';
}
// A byte-order mark (UTF-8/UTF-16/UTF-32) decoded into the string.
if (content.charCodeAt(0) === 0xfeff) {
return ' The file begins with a byte-order mark (BOM); re-save it as UTF-8 without a BOM.';
}
// NUL bytes strongly suggest the file is UTF-16/UTF-32 encoded.
if (content.includes('\x00')) {
return ' The file contains NUL bytes; it may be UTF-16/UTF-32 encoded. Re-save it as UTF-8.';
}
// Unresolved git merge-conflict markers.
if (/^(<{7}|={7}|>{7})( |$)/m.test(content)) {
return ' The file appears to contain unresolved git merge-conflict markers.';
}
return '';
}
const YAML_LOAD_OPTIONS = { json: true, schema: js_yaml_1.FAILSAFE_SCHEMA };
/**
* Parse a single-document YAML file with the option set used across this
* library. Returns null for a document-less file (empty, only comments, or
* only whitespace), matching js-yaml 4's load() behaviour - js-yaml 5 throws
* on those instead. Malformed and multi-document input rethrow js-yaml's
* original error.
*
* The document-less case is detected with loadAll(), which returns [] for it
* on both major versions, rather than by matching the human-readable text of
* the YAMLException, which is not a stable API.
*/
function loadYamlOrNull(content) {
try {
return (0, js_yaml_1.load)(content, YAML_LOAD_OPTIONS);
}
catch (e) {
try {
if ((0, js_yaml_1.loadAll)(content, YAML_LOAD_OPTIONS).length === 0) {
return null;
}
}
catch {
// fall through to rethrow the original single-document error
}
throw e;
}
}
/**
* Like loadYamlOrNull, but for files that must contain a YAML mapping
* (lockfiles). Content that parses to anything else (empty, comment-only, or
* a bare document separator) is rejected loudly with an InvalidUserInputError
* rather than silently producing an empty dependency graph.
*
* `fileLabel` names the file in the error, e.g. 'pnpm-lock.yaml'.
*/
function loadYamlMappingOrThrow(content, fileLabel) {
const parsed = loadYamlOrNull(content);
if (!parsed || typeof parsed !== 'object') {
throw new errors_1.InvalidUserInputError(`${fileLabel} parsing failed: the file is empty or does not contain a YAML mapping`);
}
return parsed;
}
//# sourceMappingURL=utils.js.map