UNPKG

snyk-nodejs-lockfile-parser

Version:
211 lines 9.3 kB
"use strict"; 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