@dependably/npm-check 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,215 @@
1
+ // src/usage-scanner.js
2
+ // Heuristic detection of dependencies declared in package.json but never
3
+ // imported by the application's source code ("flag for removal").
4
+ import fs from 'fs';
5
+ import path from 'path';
6
+
7
+ export class UsageScannerError extends Error {
8
+ constructor(message, code, context = {}) {
9
+ super(message);
10
+ this.name = 'UsageScannerError';
11
+ this.code = code;
12
+ this.context = context;
13
+ }
14
+ }
15
+
16
+ export const DEFAULT_EXTENSIONS = ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx', '.vue', '.svelte'];
17
+ export const DEFAULT_IGNORE_DIRS = [
18
+ 'node_modules', '.git', '.backups', 'dist', 'build', 'out', 'coverage', '.next', '.nuxt', 'vendor'
19
+ ];
20
+ // Build/tooling output dirs that DEFAULT_IGNORE_DIRS skips during the app scan.
21
+ // `findUnusedDependencies` scans these separately so a dependency imported only
22
+ // by a hand-written build toolkit (e.g. a `build/` shipped as source) is not
23
+ // mistaken for unused. Conservative by design: it can only rescue deps from a
24
+ // removal suggestion, never add one.
25
+ export const DEFAULT_BUILD_DIRS = ['build', 'dist', 'out'];
26
+
27
+ // require('x') / require("x") / import('x') / import "x" / from 'x' / export ... from 'x'
28
+ const IMPORT_PATTERNS = [
29
+ /\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
30
+ /\bimport\s*\(\s*['"]([^'"]+)['"]\s*\)/g,
31
+ /\bimport\s+['"]([^'"]+)['"]/g,
32
+ /\bfrom\s+['"]([^'"]+)['"]/g
33
+ ];
34
+
35
+ /**
36
+ * Reduce an import specifier to its package name.
37
+ * 'lodash/fp' → 'lodash'; '@scope/pkg/sub' → '@scope/pkg';
38
+ * relative paths and node: builtins → null.
39
+ */
40
+ export function specifierToPackageName(specifier) {
41
+ if (!specifier || typeof specifier !== 'string') return null;
42
+ if (specifier.startsWith('.') || specifier.startsWith('/') || specifier.startsWith('#')) return null;
43
+ if (specifier.startsWith('node:')) return null;
44
+ if (specifier.includes('://')) return null; // URLs (e.g. https: imports)
45
+
46
+ const parts = specifier.split('/');
47
+ if (specifier.startsWith('@')) {
48
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : null;
49
+ }
50
+ return parts[0];
51
+ }
52
+
53
+ function walkFiles(dir, extensions, ignoreDirs, files = []) {
54
+ let entries;
55
+ try {
56
+ entries = fs.readdirSync(dir, { withFileTypes: true });
57
+ } catch {
58
+ return files;
59
+ }
60
+ for (const entry of entries) {
61
+ if (entry.isDirectory()) {
62
+ if (!ignoreDirs.includes(entry.name) && !entry.name.startsWith('.')) {
63
+ walkFiles(path.join(dir, entry.name), extensions, ignoreDirs, files);
64
+ }
65
+ } else if (entry.isFile() && extensions.includes(path.extname(entry.name))) {
66
+ files.push(path.join(dir, entry.name));
67
+ }
68
+ }
69
+ return files;
70
+ }
71
+
72
+ /**
73
+ * Scan a project's source files and collect the set of imported package names.
74
+ * @param {string} dir - Project root
75
+ * @param {object} options - { extensions, ignoreDirs }
76
+ * @returns {{used: Set<string>, scannedFiles: number}}
77
+ */
78
+ export function scanUsedPackages(dir, options = {}) {
79
+ const { extensions = DEFAULT_EXTENSIONS, ignoreDirs = DEFAULT_IGNORE_DIRS } = options;
80
+
81
+ if (!fs.existsSync(dir)) {
82
+ throw new UsageScannerError(`Directory not found: ${dir}`, 'DIR_NOT_FOUND');
83
+ }
84
+
85
+ const files = walkFiles(dir, extensions, ignoreDirs);
86
+ const used = new Set();
87
+
88
+ for (const file of files) {
89
+ let content;
90
+ try {
91
+ content = fs.readFileSync(file, 'utf8');
92
+ } catch {
93
+ continue;
94
+ }
95
+ for (const pattern of IMPORT_PATTERNS) {
96
+ pattern.lastIndex = 0;
97
+ let match;
98
+ while ((match = pattern.exec(content)) !== null) {
99
+ const name = specifierToPackageName(match[1]);
100
+ if (name) used.add(name);
101
+ }
102
+ }
103
+ }
104
+
105
+ return { used, scannedFiles: files.length };
106
+ }
107
+
108
+ /**
109
+ * Decide whether a declared dependency counts as "used" — by an explicit
110
+ * ignore-list entry, a real import, an npm-script mention, or the
111
+ * @types/foo-follows-foo heuristic.
112
+ */
113
+ function isDependencyUsed(name, { used, scriptsText, ignore }) {
114
+ return (
115
+ ignore.includes(name) ||
116
+ used.has(name) ||
117
+ // CLI tools invoked from npm scripts (eslint, jest, …) are used even
118
+ // though nothing imports them
119
+ scriptsText.includes(name) ||
120
+ // @types/foo is "used" when foo itself is
121
+ (name.startsWith('@types/') && used.has(name.slice('@types/'.length)))
122
+ );
123
+ }
124
+
125
+ /**
126
+ * Collect the unused entries from a single package.json dependency section.
127
+ */
128
+ function collectUnusedInSection(deps, section, context) {
129
+ const unused = [];
130
+ if (!deps || typeof deps !== 'object') return unused;
131
+
132
+ for (const [name, version] of Object.entries(deps)) {
133
+ if (isDependencyUsed(name, context)) continue;
134
+ unused.push({ name, section, version });
135
+ }
136
+ return unused;
137
+ }
138
+
139
+ /**
140
+ * Scan each build/tooling dir that exists under `dir`, as its own pass. Returns
141
+ * the union of packages imported there plus a per-dir file/usage breakdown.
142
+ */
143
+ function scanBuildDirs(dir, buildDirs, options) {
144
+ const used = new Set();
145
+ let scannedFiles = 0;
146
+ const dirsScanned = [];
147
+ for (const name of buildDirs) {
148
+ const full = path.join(dir, name);
149
+ if (!fs.existsSync(full)) continue;
150
+ const res = scanUsedPackages(full, options);
151
+ for (const pkg of res.used) used.add(pkg);
152
+ scannedFiles += res.scannedFiles;
153
+ dirsScanned.push(name);
154
+ }
155
+ return { used, scannedFiles, dirsScanned };
156
+ }
157
+
158
+ /**
159
+ * Find dependencies declared in package.json that the application never
160
+ * imports. Heuristic — results are flagged for removal, never auto-removed:
161
+ * packages used only via CLI, config files, or runtime magic can appear
162
+ * unused. Mentions in npm scripts count as used to reduce CLI-tool noise.
163
+ *
164
+ * Runs two passes: the application (with build/output dirs ignored), and the
165
+ * build/tooling dirs (`buildDirs`) separately. A dependency counts as used if
166
+ * either pass imports it, so a `build/` shipped as source no longer produces
167
+ * false "unused" flags; deps imported ONLY by the build pass are surfaced as
168
+ * `buildOnly` for visibility. Pass `buildDirs: []` to disable the second pass.
169
+ *
170
+ * @param {object} packageJson - Parsed package.json
171
+ * @param {string} dir - Project root to scan
172
+ * @param {object} options - { includeDev = false, ignore = [], buildDirs, extensions, ignoreDirs }
173
+ * @returns {{unused: Array<{name, section, version}>, used: Set<string>, usedByApp: Set<string>, usedByBuild: Set<string>, buildOnly: string[], scannedFiles: number, appFiles: number, buildFiles: number, buildDirsScanned: string[], sectionsChecked: string[]}}
174
+ */
175
+ export function findUnusedDependencies(packageJson, dir, options = {}) {
176
+ const { includeDev = false, ignore = [], buildDirs = DEFAULT_BUILD_DIRS } = options;
177
+
178
+ if (!packageJson || typeof packageJson !== 'object') {
179
+ throw new UsageScannerError('package.json data is required', 'MISSING_PACKAGE_JSON');
180
+ }
181
+
182
+ // Pass 1: the application, with build/output dirs ignored (default behavior).
183
+ const app = scanUsedPackages(dir, options);
184
+ const usedByApp = app.used;
185
+
186
+ // Pass 2: the build/tooling dirs, scanned separately.
187
+ const build = scanBuildDirs(dir, buildDirs, options);
188
+ const usedByBuild = build.used;
189
+
190
+ const used = new Set([...usedByApp, ...usedByBuild]);
191
+ const scriptsText = Object.values(packageJson.scripts || {}).join('\n');
192
+ const context = { used, scriptsText, ignore };
193
+
194
+ const sectionsChecked = includeDev ? ['dependencies', 'devDependencies'] : ['dependencies'];
195
+ const unused = [];
196
+
197
+ for (const section of sectionsChecked) {
198
+ unused.push(...collectUnusedInSection(packageJson[section], section, context));
199
+ }
200
+
201
+ const buildOnly = [...usedByBuild].filter((name) => !usedByApp.has(name)).sort();
202
+
203
+ return {
204
+ unused,
205
+ used,
206
+ usedByApp,
207
+ usedByBuild,
208
+ buildOnly,
209
+ scannedFiles: app.scannedFiles + build.scannedFiles,
210
+ appFiles: app.scannedFiles,
211
+ buildFiles: build.scannedFiles,
212
+ buildDirsScanned: build.dirsScanned,
213
+ sectionsChecked
214
+ };
215
+ }
@@ -0,0 +1,282 @@
1
+ // src/validator.js
2
+ import { detectLockfileVersion, hasPackagesMap, hasDependenciesTree } from './format-library.js';
3
+
4
+ export class ValidationError extends Error {
5
+ constructor(message, code) {
6
+ super(message);
7
+ this.code = code;
8
+ }
9
+ }
10
+
11
+ // Allow calling validatePackageLock(lockfile, options) when packageJson is omitted.
12
+ function normalizeValidateArgs(argLength, packageJson, options) {
13
+ const looksLikeOptions = argLength === 2 && packageJson && typeof packageJson === 'object' &&
14
+ (packageJson.allowMissingIntegrity !== undefined || packageJson.validateAgainstPackageJson !== undefined);
15
+ if (looksLikeOptions) {
16
+ return { packageJson: null, options: packageJson };
17
+ }
18
+ return { packageJson, options };
19
+ }
20
+
21
+ // Basic top-level field checks (name, version, lockfileVersion).
22
+ function validateRootFields(lockfile, errors) {
23
+ if (!lockfile.name || typeof lockfile.name !== 'string') {
24
+ errors.push(new ValidationError('Missing or invalid package name', 'INVALID_NAME'));
25
+ }
26
+ if (!lockfile.version || typeof lockfile.version !== 'string') {
27
+ errors.push(new ValidationError('Missing or invalid package version', 'INVALID_VERSION'));
28
+ }
29
+ // Basic semver-ish check (major.minor.patch). If format is wrong, mark invalid.
30
+ if (typeof lockfile.version === 'string') {
31
+ const semverLike = /^\d+\.\d+\.\d+(?:[-+].*)?$/;
32
+ if (!semverLike.test(lockfile.version)) {
33
+ errors.push(new ValidationError('Missing or invalid package version', 'INVALID_VERSION'));
34
+ }
35
+ }
36
+ if (typeof lockfile.lockfileVersion !== 'number') {
37
+ errors.push(new ValidationError('Missing or invalid lockfile version', 'INVALID_LOCKFILE_VERSION'));
38
+ }
39
+ }
40
+
41
+ // Validate the version-appropriate dependencies tree and/or packages map.
42
+ function validateVersionStructure(lockfile, version, errors, warnings, options) {
43
+ if (hasDependenciesTree(version)) {
44
+ if (!lockfile.dependencies || typeof lockfile.dependencies !== 'object') {
45
+ errors.push(new ValidationError('Missing dependencies object', 'MISSING_DEPENDENCIES'));
46
+ } else {
47
+ validateDependenciesTree(lockfile.dependencies, errors, warnings);
48
+ }
49
+ }
50
+
51
+ if (hasPackagesMap(version)) {
52
+ if (!lockfile.packages || typeof lockfile.packages !== 'object') {
53
+ errors.push(new ValidationError('Missing packages map', 'MISSING_PACKAGES_MAP'));
54
+ } else {
55
+ validatePackagesMap(lockfile.packages, errors, warnings, options);
56
+ }
57
+ }
58
+ }
59
+
60
+ export function validatePackageLock(lockfile, packageJson = null, options = {}) {
61
+ ({ packageJson, options } = normalizeValidateArgs(arguments.length, packageJson, options));
62
+ const errors = [];
63
+ const warnings = [];
64
+ const info = {};
65
+
66
+ // Basic structure checks
67
+ validateRootFields(lockfile, errors);
68
+
69
+ let version;
70
+ try {
71
+ version = detectLockfileVersion(lockfile);
72
+ info.version = version;
73
+ } catch (e) {
74
+ // Normalize error code expected by tests
75
+ errors.push(new ValidationError(e.message, 'VERSION_MISMATCH'));
76
+ return { valid: false, errors, warnings, info };
77
+ }
78
+
79
+ validateVersionStructure(lockfile, version, errors, warnings, options);
80
+
81
+ // Validate against package.json if provided
82
+ if (packageJson && options.validateAgainstPackageJson) {
83
+ validateAgainstPackageJson(lockfile, packageJson, errors);
84
+ }
85
+
86
+ const valid = errors.length === 0 && !(options.strictMode && warnings.length > 0);
87
+ return { valid, errors, warnings, info };
88
+ }
89
+
90
+ // Recognise one SRI token: sha1/sha256/sha384/sha512.
91
+ const SRI_TOKEN_REGEX = /^sha(?:1|256|384|512)-[A-Za-z0-9+/=]+$/;
92
+
93
+ // Accept sha1/sha256/sha384/sha512 single hashes and space-separated multi-hash
94
+ // SRI strings (e.g. 'sha512-... sha1-...'). Every whitespace-separated token
95
+ // must match a recognised algorithm.
96
+ function isValidIntegrityHash(value) {
97
+ if (typeof value !== 'string' || value.trim() === '') return false;
98
+ return value.trim().split(/\s+/).every(token => SRI_TOKEN_REGEX.test(token));
99
+ }
100
+
101
+ // Returns true when any token in a (valid) integrity value uses the legacy sha1
102
+ // algorithm. Callers should emit a warning (not an error) suggesting an upgrade.
103
+ function hasLegacySha1(value) {
104
+ if (typeof value !== 'string') return false;
105
+ return value.trim().split(/\s+/).some(token => token.startsWith('sha1-'));
106
+ }
107
+
108
+ // Classify a dependency entry by shape. Returns 'leaf' for string/boolean leaves,
109
+ // 'object' for valid objects, or 'invalid' for anything that isn't an object.
110
+ function classifyDependencyEntry(dep) {
111
+ if (typeof dep === 'string' || typeof dep === 'boolean') {
112
+ return 'leaf';
113
+ }
114
+ if (typeof dep !== 'object' || Array.isArray(dep)) {
115
+ return 'invalid';
116
+ }
117
+ return 'object';
118
+ }
119
+
120
+ // Validate a single dependency entry; recurses into nested dependencies.
121
+ // warnings is threaded through so sha1 hashes can be flagged without an error.
122
+ function validateDependencyEntry(name, dep, errors, warnings, depth) {
123
+ if (dep == null) {
124
+ errors.push(new ValidationError(`Dependency ${name} is not an object`, 'INVALID_DEPENDENCY'));
125
+ return;
126
+ }
127
+
128
+ const kind = classifyDependencyEntry(dep);
129
+ if (kind === 'invalid') {
130
+ errors.push(new ValidationError(`Dependency ${name} is not an object`, 'INVALID_DEPENDENCY'));
131
+ return;
132
+ }
133
+ if (kind === 'leaf') {
134
+ // Top-level string/boolean entries are missing required fields; nested
135
+ // string/boolean entries are valid range/flag leaves.
136
+ if (depth === 0) {
137
+ errors.push(new ValidationError(`Missing or invalid version for ${name}`, 'MISSING_DEP_VERSION'));
138
+ }
139
+ return;
140
+ }
141
+
142
+ // Validate version when dependency is an object
143
+ if (!dep.version || typeof dep.version !== 'string') {
144
+ errors.push(new ValidationError(`Missing or invalid version for ${name}`, 'MISSING_DEP_VERSION'));
145
+ }
146
+
147
+ // Validate integrity on dependency objects if present.
148
+ // sha1 is accepted as structurally valid but emits a LEGACY_INTEGRITY warning.
149
+ if (dep.integrity) {
150
+ if (!isValidIntegrityHash(dep.integrity)) {
151
+ errors.push(new ValidationError(`Invalid integrity hash for dependency ${name}`, 'INVALID_INTEGRITY'));
152
+ } else if (hasLegacySha1(dep.integrity)) {
153
+ warnings.push({ code: 'LEGACY_INTEGRITY', message: `Dependency ${name} uses sha1 integrity; run upgrade-hashes to convert to sha512` });
154
+ }
155
+ }
156
+
157
+ if (dep.dependencies) {
158
+ validateDependenciesTree(dep.dependencies, errors, warnings, depth + 1);
159
+ }
160
+ }
161
+
162
+ function validateDependenciesTree(dependencies, errors, warnings, depth = 0) {
163
+ for (const [name, dep] of Object.entries(dependencies)) {
164
+ validateDependencyEntry(name, dep, errors, warnings, depth);
165
+ }
166
+ }
167
+
168
+ // Validate the integrity field (and the allowMissingIntegrity policy) for a package entry.
169
+ // sha1 integrity is accepted as structurally valid but emits a LEGACY_INTEGRITY warning.
170
+ function validatePackageIntegrity(path, pkg, errors, warnings, options) {
171
+ if (pkg.integrity) {
172
+ if (!isValidIntegrityHash(pkg.integrity)) {
173
+ errors.push(new ValidationError(`Invalid integrity hash for package at ${path}`, 'INVALID_INTEGRITY'));
174
+ } else if (hasLegacySha1(pkg.integrity)) {
175
+ warnings.push({ code: 'LEGACY_INTEGRITY', message: `Package at ${path} uses sha1 integrity; run upgrade-hashes to convert to sha512` });
176
+ }
177
+ }
178
+ if (options.allowMissingIntegrity === false && !pkg.integrity) {
179
+ // Treat missing integrity as an error when not allowed, and also record a warning
180
+ errors.push(new ValidationError(`Missing integrity hash for package at ${path}`, 'MISSING_INTEGRITY'));
181
+ warnings.push({ code: 'MISSING_INTEGRITY', message: `Missing integrity hash for package at ${path}` });
182
+ }
183
+ }
184
+
185
+ // Validate the resolved-URL scheme for a package entry.
186
+ // Guards typeof before calling startsWith to avoid a crash on corrupted
187
+ // non-string resolved values (e.g. "resolved": 42 from a bad merge).
188
+ function validatePackageResolved(path, pkg, warnings) {
189
+ if (!pkg.resolved) {
190
+ return;
191
+ }
192
+ if (typeof pkg.resolved !== 'string') {
193
+ warnings.push({ code: 'INVALID_RESOLVED', message: `Invalid resolved URL for package at ${path}: ${pkg.resolved}` });
194
+ return;
195
+ }
196
+ const validSchemes = ['https://', 'http://', 'git+', 'git://', 'file:'];
197
+ const hasValidScheme = validSchemes.some(scheme => pkg.resolved.startsWith(scheme));
198
+ if (!hasValidScheme) {
199
+ warnings.push({ code: 'INVALID_RESOLVED', message: `Invalid resolved URL for package at ${path}: ${pkg.resolved}` });
200
+ }
201
+ }
202
+
203
+ // Recurse into each dependency section of a package entry. packages-map
204
+ // dependency values are version-range strings (depth 1 allows string/boolean
205
+ // leaves), unlike the v1 top-level tree.
206
+ function validatePackageDependencySections(pkg, errors, warnings) {
207
+ for (const section of ['dependencies', 'devDependencies', 'peerDependencies', 'optionalDependencies']) {
208
+ if (pkg[section]) {
209
+ validateDependenciesTree(pkg[section], errors, warnings, 1);
210
+ }
211
+ }
212
+ }
213
+
214
+ // Validate a single packages-map entry.
215
+ function validatePackageEntry(path, pkg, errors, warnings, options) {
216
+ if (!pkg || typeof pkg !== 'object') {
217
+ errors.push(new ValidationError(`Package at ${path} is not an object`, 'INVALID_PACKAGE'));
218
+ return;
219
+ }
220
+ // npm only writes `name` on the root entry (and aliased installs);
221
+ // requiring it elsewhere would reject every real lockfile
222
+ if (path === '' && (!pkg.name || typeof pkg.name !== 'string')) {
223
+ errors.push(new ValidationError(`Missing or invalid name for package at ${path}`, 'INVALID_PACKAGE_NAME'));
224
+ }
225
+ // link entries (`link: true`) carry no version by design
226
+ if (!pkg.link && (!pkg.version || typeof pkg.version !== 'string')) {
227
+ errors.push(new ValidationError(`Missing or invalid version for package at ${path}`, 'INVALID_PACKAGE_VERSION'));
228
+ }
229
+ validatePackageIntegrity(path, pkg, errors, warnings, options);
230
+ validatePackageResolved(path, pkg, warnings);
231
+ validatePackageDependencySections(pkg, errors, warnings);
232
+ }
233
+
234
+ function validatePackagesMap(packages, errors, warnings, options) {
235
+ for (const [path, pkg] of Object.entries(packages)) {
236
+ validatePackageEntry(path, pkg, errors, warnings, options);
237
+ }
238
+ }
239
+
240
+ // Check that every package.json dependency in a section is present in the
241
+ // lockfile root entry's matching section (v2/v3 packages map path).
242
+ function checkDependencySectionInLockfile(rootEntry, packageJson, section, label, code, errors) {
243
+ const lockDeps = rootEntry && rootEntry[section];
244
+ const pkgDeps = packageJson[section] || {};
245
+ for (const [name] of Object.entries(pkgDeps)) {
246
+ if (!lockDeps || !lockDeps[name]) {
247
+ errors.push(new ValidationError(`Missing ${label} ${name} in lockfile`, code));
248
+ }
249
+ }
250
+ }
251
+
252
+ // v1 lockfile cross-check: each package.json dep section is checked against
253
+ // the top-level dependencies tree (v1 hoists all deps to the top level).
254
+ function checkDependencySectionInV1Tree(v1Deps, packageJson, section, label, code, errors) {
255
+ const pkgDeps = packageJson[section] || {};
256
+ for (const [name] of Object.entries(pkgDeps)) {
257
+ if (!v1Deps[name]) {
258
+ errors.push(new ValidationError(`Missing ${label} ${name} in lockfile`, code));
259
+ }
260
+ }
261
+ }
262
+
263
+ function validateAgainstPackageJson(lockfile, packageJson, errors) {
264
+ const rootEntry = lockfile.packages && lockfile.packages[''];
265
+
266
+ if (!rootEntry) {
267
+ // v1 lockfile (or a degenerate v2/v3 missing the root packages[''] entry):
268
+ // fall back to the top-level dependencies tree for the cross-check.
269
+ if (lockfile.dependencies) {
270
+ checkDependencySectionInV1Tree(lockfile.dependencies, packageJson, 'dependencies', 'dependency', 'MISSING_IN_LOCKFILE', errors);
271
+ checkDependencySectionInV1Tree(lockfile.dependencies, packageJson, 'devDependencies', 'devDependency', 'MISSING_DEV_IN_LOCKFILE', errors);
272
+ checkDependencySectionInV1Tree(lockfile.dependencies, packageJson, 'optionalDependencies', 'optionalDependency', 'MISSING_OPT_IN_LOCKFILE', errors);
273
+ checkDependencySectionInV1Tree(lockfile.dependencies, packageJson, 'peerDependencies', 'peerDependency', 'MISSING_PEER_IN_LOCKFILE', errors);
274
+ }
275
+ return;
276
+ }
277
+
278
+ checkDependencySectionInLockfile(rootEntry, packageJson, 'dependencies', 'dependency', 'MISSING_IN_LOCKFILE', errors);
279
+ checkDependencySectionInLockfile(rootEntry, packageJson, 'devDependencies', 'devDependency', 'MISSING_DEV_IN_LOCKFILE', errors);
280
+ checkDependencySectionInLockfile(rootEntry, packageJson, 'optionalDependencies', 'optionalDependency', 'MISSING_OPT_IN_LOCKFILE', errors);
281
+ checkDependencySectionInLockfile(rootEntry, packageJson, 'peerDependencies', 'peerDependency', 'MISSING_PEER_IN_LOCKFILE', errors);
282
+ }