flatlock 1.0.1 → 1.2.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.
- package/README.md +55 -2
- package/bin/flatlock-cmp.js +109 -356
- package/dist/compare.d.ts +85 -0
- package/dist/compare.d.ts.map +1 -0
- package/dist/detect.d.ts +33 -0
- package/dist/detect.d.ts.map +1 -0
- package/dist/index.d.ts +60 -20
- package/dist/index.d.ts.map +1 -1
- package/dist/parsers/index.d.ts +5 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/npm.d.ts +109 -0
- package/dist/parsers/npm.d.ts.map +1 -0
- package/dist/parsers/pnpm/detect.d.ts +136 -0
- package/dist/parsers/pnpm/detect.d.ts.map +1 -0
- package/dist/parsers/pnpm/index.d.ts +120 -0
- package/dist/parsers/pnpm/index.d.ts.map +1 -0
- package/dist/parsers/pnpm/internal.d.ts +5 -0
- package/dist/parsers/pnpm/internal.d.ts.map +1 -0
- package/dist/parsers/pnpm/shrinkwrap.d.ts +129 -0
- package/dist/parsers/pnpm/shrinkwrap.d.ts.map +1 -0
- package/dist/parsers/pnpm/v5.d.ts +139 -0
- package/dist/parsers/pnpm/v5.d.ts.map +1 -0
- package/dist/parsers/pnpm/v6plus.d.ts +212 -0
- package/dist/parsers/pnpm/v6plus.d.ts.map +1 -0
- package/dist/parsers/pnpm.d.ts +2 -0
- package/dist/parsers/pnpm.d.ts.map +1 -0
- package/dist/parsers/types.d.ts +23 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/yarn-berry.d.ts +154 -0
- package/dist/parsers/yarn-berry.d.ts.map +1 -0
- package/dist/parsers/yarn-classic.d.ts +110 -0
- package/dist/parsers/yarn-classic.d.ts.map +1 -0
- package/dist/result.d.ts +12 -0
- package/dist/result.d.ts.map +1 -0
- package/dist/set.d.ts +189 -0
- package/dist/set.d.ts.map +1 -0
- package/package.json +18 -7
- package/src/compare.js +620 -0
- package/src/detect.js +8 -7
- package/src/index.js +33 -15
- package/src/parsers/index.js +12 -4
- package/src/parsers/npm.js +70 -23
- package/src/parsers/pnpm/detect.js +198 -0
- package/src/parsers/pnpm/index.js +289 -0
- package/src/parsers/pnpm/internal.js +41 -0
- package/src/parsers/pnpm/shrinkwrap.js +241 -0
- package/src/parsers/pnpm/v5.js +225 -0
- package/src/parsers/pnpm/v6plus.js +290 -0
- package/src/parsers/pnpm.js +12 -77
- package/src/parsers/types.js +10 -0
- package/src/parsers/yarn-berry.js +187 -38
- package/src/parsers/yarn-classic.js +85 -24
- package/src/result.js +2 -2
- package/src/set.js +618 -0
- package/src/types.d.ts +54 -0
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview pnpm lockfile parser supporting all documented versions
|
|
3
|
+
*
|
|
4
|
+
* Supported formats:
|
|
5
|
+
* - shrinkwrap.yaml v3/v4 (2016-2019): shrinkwrapVersion field
|
|
6
|
+
* - pnpm-lock.yaml v5.x (2019-2022): lockfileVersion number
|
|
7
|
+
* - pnpm-lock.yaml v5.4-inlineSpecifiers (experimental): lockfileVersion string
|
|
8
|
+
* - pnpm-lock.yaml v6.0 (2023): lockfileVersion '6.0'
|
|
9
|
+
* - pnpm-lock.yaml v9.0 (2024+): lockfileVersion '9.0'
|
|
10
|
+
*
|
|
11
|
+
* @module flatlock/parsers/pnpm
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import yaml from 'js-yaml';
|
|
15
|
+
|
|
16
|
+
import { detectVersion } from './detect.js';
|
|
17
|
+
import { parseSpecShrinkwrap } from './shrinkwrap.js';
|
|
18
|
+
import { parseSpecV5 } from './v5.js';
|
|
19
|
+
import { parseSpecV6Plus } from './v6plus.js';
|
|
20
|
+
|
|
21
|
+
/** @typedef {import('../types.js').Dependency} Dependency */
|
|
22
|
+
|
|
23
|
+
// Public API: detectVersion for users who need to inspect lockfile version
|
|
24
|
+
export { detectVersion } from './detect.js';
|
|
25
|
+
|
|
26
|
+
// Version-specific internals available via 'flatlock/parsers/pnpm/internal'
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse pnpm package spec to extract name and version.
|
|
30
|
+
*
|
|
31
|
+
* This is the unified parser that auto-detects the format based on the spec pattern.
|
|
32
|
+
* It supports all pnpm lockfile versions without requiring version context.
|
|
33
|
+
*
|
|
34
|
+
* Detection heuristics:
|
|
35
|
+
* 1. If spec contains '(' -> v6+ format (peer deps in parentheses)
|
|
36
|
+
* 2. If spec contains '@' after position 0 and no '/' after the '@' -> v6+ format
|
|
37
|
+
* 3. Otherwise -> v5 or earlier format (slash separator)
|
|
38
|
+
*
|
|
39
|
+
* @param {string} spec - Package spec from pnpm lockfile
|
|
40
|
+
* @returns {{ name: string | null, version: string | null }}
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // v5 format - unscoped package
|
|
44
|
+
* parseSpec('/lodash/4.17.21')
|
|
45
|
+
* // => { name: 'lodash', version: '4.17.21' }
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // v5 format - scoped package
|
|
49
|
+
* parseSpec('/@babel/core/7.23.0')
|
|
50
|
+
* // => { name: '@babel/core', version: '7.23.0' }
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // v5 format - with peer dependency suffix (underscore)
|
|
54
|
+
* parseSpec('/styled-jsx/3.0.9_react@17.0.2')
|
|
55
|
+
* // => { name: 'styled-jsx', version: '3.0.9' }
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // v6 format - unscoped package (with leading slash)
|
|
59
|
+
* parseSpec('/lodash@4.17.21')
|
|
60
|
+
* // => { name: 'lodash', version: '4.17.21' }
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // v6 format - scoped package
|
|
64
|
+
* parseSpec('/@babel/core@7.23.0')
|
|
65
|
+
* // => { name: '@babel/core', version: '7.23.0' }
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // v9 format - unscoped package (no leading slash)
|
|
69
|
+
* parseSpec('lodash@4.17.21')
|
|
70
|
+
* // => { name: 'lodash', version: '4.17.21' }
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // v9 format - scoped package (no leading slash)
|
|
74
|
+
* parseSpec('@babel/core@7.23.0')
|
|
75
|
+
* // => { name: '@babel/core', version: '7.23.0' }
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // v9 format - with peer dependency suffix (parentheses)
|
|
79
|
+
* parseSpec('@babel/core@7.23.0(@types/node@20.0.0)')
|
|
80
|
+
* // => { name: '@babel/core', version: '7.23.0' }
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // v9 format - multiple peer dependencies
|
|
84
|
+
* parseSpec('@testing-library/react@14.0.0(react-dom@18.2.0)(react@18.2.0)')
|
|
85
|
+
* // => { name: '@testing-library/react', version: '14.0.0' }
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Shrinkwrap v3/v4 format - with peer suffix (slash)
|
|
89
|
+
* parseSpec('/foo/1.0.0/bar@2.0.0')
|
|
90
|
+
* // => { name: 'foo', version: '1.0.0' }
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // link: protocol - skipped (returns null)
|
|
94
|
+
* parseSpec('link:packages/my-pkg')
|
|
95
|
+
* // => { name: null, version: null }
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // file: protocol - skipped (returns null)
|
|
99
|
+
* parseSpec('file:../local-package')
|
|
100
|
+
* // => { name: null, version: null }
|
|
101
|
+
*
|
|
102
|
+
* @example
|
|
103
|
+
* // Null input
|
|
104
|
+
* parseSpec(null)
|
|
105
|
+
* // => { name: null, version: null }
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Prerelease version
|
|
109
|
+
* parseSpec('@verdaccio/ui-theme@6.0.0-6-next.50')
|
|
110
|
+
* // => { name: '@verdaccio/ui-theme', version: '6.0.0-6-next.50' }
|
|
111
|
+
*/
|
|
112
|
+
export function parseSpec(spec) {
|
|
113
|
+
// Handle null/undefined input
|
|
114
|
+
if (spec == null || typeof spec !== 'string') {
|
|
115
|
+
return { name: null, version: null };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Skip special protocols
|
|
119
|
+
if (spec.startsWith('link:') || spec.startsWith('file:')) {
|
|
120
|
+
return { name: null, version: null };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Detect format based on spec pattern
|
|
124
|
+
// v6+ uses parentheses for peer deps and @ separator
|
|
125
|
+
// v5 and earlier use _ for peer deps and / separator
|
|
126
|
+
|
|
127
|
+
// Check for v6+ parentheses peer suffix
|
|
128
|
+
if (spec.includes('(')) {
|
|
129
|
+
return parseSpecV6Plus(spec);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Remove leading slash for analysis
|
|
133
|
+
const cleaned = spec.startsWith('/') ? spec.slice(1) : spec;
|
|
134
|
+
|
|
135
|
+
// Check for v6+ @ separator format
|
|
136
|
+
// In v6+, the format is name@version where @ separates name from version
|
|
137
|
+
// We need to find @ that isn't at position 0 (which would be a scope)
|
|
138
|
+
// And check that it's not part of a v5 peer suffix (which comes after _)
|
|
139
|
+
|
|
140
|
+
// First strip any v5 peer suffix for cleaner analysis
|
|
141
|
+
const withoutV5Peer = cleaned.split('_')[0];
|
|
142
|
+
|
|
143
|
+
// Find the last @ in the cleaned string
|
|
144
|
+
const lastAtIndex = withoutV5Peer.lastIndexOf('@');
|
|
145
|
+
|
|
146
|
+
if (lastAtIndex > 0) {
|
|
147
|
+
// Check if this is v6+ format by seeing if there's a / after the @
|
|
148
|
+
// In v6+: @babel/core@7.23.0 - the last @ separates name@version
|
|
149
|
+
// In v5: @babel/core/7.23.0 - no @ after the scope
|
|
150
|
+
|
|
151
|
+
const afterAt = withoutV5Peer.slice(lastAtIndex + 1);
|
|
152
|
+
|
|
153
|
+
// If there's no / in the part after the last @, it's likely v6+ format
|
|
154
|
+
// (the part after @ is just the version like "7.23.0")
|
|
155
|
+
if (!afterAt.includes('/')) {
|
|
156
|
+
return parseSpecV6Plus(spec);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Fall back to v5 format (also handles shrinkwrap v3/v4 for basic cases)
|
|
161
|
+
// Note: shrinkwrap v3/v4 peer suffix with / is handled differently,
|
|
162
|
+
// but parseSpecV5 will still extract the correct name/version
|
|
163
|
+
// because it stops at _ (v5) and the shrinkwrap / peer suffix
|
|
164
|
+
// comes after the version anyway
|
|
165
|
+
|
|
166
|
+
return parseSpecV5(spec);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Extract package name from pnpm lockfile key.
|
|
171
|
+
* Wraps parseSpec to return just the name (consistent with other parsers).
|
|
172
|
+
*
|
|
173
|
+
* @param {string} key - pnpm lockfile key
|
|
174
|
+
* @returns {string | null} Package name
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* parseLockfileKey('/@babel/core@7.23.0') // => '@babel/core'
|
|
178
|
+
* parseLockfileKey('/lodash/4.17.21') // => 'lodash'
|
|
179
|
+
*/
|
|
180
|
+
export function parseLockfileKey(key) {
|
|
181
|
+
return parseSpec(key).name;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Parse pnpm lockfile (shrinkwrap.yaml, pnpm-lock.yaml v5.x, v6, v9)
|
|
186
|
+
*
|
|
187
|
+
* @param {string | object} input - Lockfile content string or pre-parsed object
|
|
188
|
+
* @param {Object} [_options] - Parser options (unused, reserved for future use)
|
|
189
|
+
* @returns {Generator<Dependency>}
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* // Parse from string
|
|
193
|
+
* const deps = [...fromPnpmLock(yamlContent)];
|
|
194
|
+
*
|
|
195
|
+
* @example
|
|
196
|
+
* // Parse from pre-parsed object
|
|
197
|
+
* const lockfile = yaml.load(content);
|
|
198
|
+
* const deps = [...fromPnpmLock(lockfile)];
|
|
199
|
+
*/
|
|
200
|
+
export function* fromPnpmLock(input, _options = {}) {
|
|
201
|
+
const lockfile = /** @type {Record<string, any>} */ (
|
|
202
|
+
typeof input === 'string' ? yaml.load(input) : input
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
// Detect version to determine where to look for packages
|
|
206
|
+
const detected = detectVersion(lockfile);
|
|
207
|
+
|
|
208
|
+
// Select era-specific parser
|
|
209
|
+
// Each era has different peer suffix formats that parseSpec can't auto-detect
|
|
210
|
+
const parseSpecForEra =
|
|
211
|
+
detected.era === 'shrinkwrap'
|
|
212
|
+
? parseSpecShrinkwrap
|
|
213
|
+
: detected.era === 'v5'
|
|
214
|
+
? parseSpecV5
|
|
215
|
+
: parseSpecV6Plus;
|
|
216
|
+
|
|
217
|
+
// Get packages object - location varies by version
|
|
218
|
+
// v5, v6: packages section directly
|
|
219
|
+
// v9: packages section has metadata, snapshots has relationships
|
|
220
|
+
// For dependency extraction, we primarily need packages (for resolution info)
|
|
221
|
+
const packages = lockfile.packages || {};
|
|
222
|
+
|
|
223
|
+
// For v9, we should also look at snapshots for additional entries
|
|
224
|
+
// that might only be in snapshots (peer variants)
|
|
225
|
+
const snapshots = lockfile.snapshots || {};
|
|
226
|
+
|
|
227
|
+
// Track seen packages to avoid duplicates (v9 has same package in both sections)
|
|
228
|
+
const seen = new Set();
|
|
229
|
+
|
|
230
|
+
// Process packages section
|
|
231
|
+
for (const [spec, pkg] of Object.entries(packages)) {
|
|
232
|
+
// Skip if we couldn't parse name/version
|
|
233
|
+
const { name, version } = parseSpecForEra(spec);
|
|
234
|
+
if (!name || !version) continue;
|
|
235
|
+
|
|
236
|
+
// Create dedup key
|
|
237
|
+
const key = `${name}@${version}`;
|
|
238
|
+
if (seen.has(key)) continue;
|
|
239
|
+
seen.add(key);
|
|
240
|
+
|
|
241
|
+
const resolution = pkg.resolution || {};
|
|
242
|
+
const integrity = resolution.integrity;
|
|
243
|
+
const resolved = resolution.tarball;
|
|
244
|
+
const link = spec.startsWith('link:') || resolution.type === 'directory';
|
|
245
|
+
|
|
246
|
+
// Skip workspace/link entries - flatlock only cares about external dependencies
|
|
247
|
+
if (link) continue;
|
|
248
|
+
|
|
249
|
+
/** @type {Dependency} */
|
|
250
|
+
const dep = { name, version };
|
|
251
|
+
if (integrity) dep.integrity = integrity;
|
|
252
|
+
if (resolved) dep.resolved = resolved;
|
|
253
|
+
yield dep;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// For v9, also process snapshots for peer variants
|
|
257
|
+
// (they might have different resolution info)
|
|
258
|
+
if (detected.era === 'v9') {
|
|
259
|
+
for (const [spec, _snapshot] of Object.entries(snapshots)) {
|
|
260
|
+
const { name, version } = parseSpecForEra(spec);
|
|
261
|
+
if (!name || !version) continue;
|
|
262
|
+
|
|
263
|
+
// Create dedup key
|
|
264
|
+
const key = `${name}@${version}`;
|
|
265
|
+
if (seen.has(key)) continue;
|
|
266
|
+
seen.add(key);
|
|
267
|
+
|
|
268
|
+
// Snapshots don't have resolution info, check if base package exists
|
|
269
|
+
// The base package key in v9 is just name@version (without peer suffix)
|
|
270
|
+
const baseKey = `${name}@${version}`;
|
|
271
|
+
const basePkg = packages[baseKey];
|
|
272
|
+
|
|
273
|
+
if (basePkg) {
|
|
274
|
+
const resolution = basePkg.resolution || {};
|
|
275
|
+
const integrity = resolution.integrity;
|
|
276
|
+
const resolved = resolution.tarball;
|
|
277
|
+
|
|
278
|
+
/** @type {Dependency} */
|
|
279
|
+
const dep = { name, version };
|
|
280
|
+
if (integrity) dep.integrity = integrity;
|
|
281
|
+
if (resolved) dep.resolved = resolved;
|
|
282
|
+
yield dep;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Note: importers (workspace packages) are intentionally NOT yielded
|
|
288
|
+
// flatlock only cares about external dependencies
|
|
289
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Internal/advanced pnpm parser exports
|
|
3
|
+
*
|
|
4
|
+
* This module exports version-specific parsing utilities for advanced use cases
|
|
5
|
+
* such as testing, debugging, or when you need fine-grained control over parsing.
|
|
6
|
+
*
|
|
7
|
+
* For normal usage, import from 'flatlock' or 'flatlock/parsers/pnpm' instead.
|
|
8
|
+
*
|
|
9
|
+
* @module flatlock/parsers/pnpm/internal
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
// Detection utilities
|
|
13
|
+
export {
|
|
14
|
+
detectVersion,
|
|
15
|
+
hasLeadingSlash,
|
|
16
|
+
usesAtSeparator,
|
|
17
|
+
usesInlineSpecifiers,
|
|
18
|
+
usesSnapshotsSplit
|
|
19
|
+
} from './detect.js';
|
|
20
|
+
|
|
21
|
+
// Shrinkwrap v3/v4 (2016-2019)
|
|
22
|
+
export {
|
|
23
|
+
extractPeerSuffix,
|
|
24
|
+
hasPeerSuffix,
|
|
25
|
+
parseSpecShrinkwrap
|
|
26
|
+
} from './shrinkwrap.js';
|
|
27
|
+
|
|
28
|
+
// v5.x (2019-2022)
|
|
29
|
+
export {
|
|
30
|
+
extractPeerSuffixV5,
|
|
31
|
+
hasPeerSuffixV5,
|
|
32
|
+
parseSpecV5
|
|
33
|
+
} from './v5.js';
|
|
34
|
+
|
|
35
|
+
// v6+ (2023+)
|
|
36
|
+
export {
|
|
37
|
+
extractPeerSuffixV6Plus,
|
|
38
|
+
hasPeerSuffixV6Plus,
|
|
39
|
+
parsePeerDependencies,
|
|
40
|
+
parseSpecV6Plus
|
|
41
|
+
} from './v6plus.js';
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Parser for pnpm shrinkwrap.yaml (v3/v4) format
|
|
3
|
+
*
|
|
4
|
+
* Shrinkwrap format (2016-2019) characteristics:
|
|
5
|
+
* - File: shrinkwrap.yaml
|
|
6
|
+
* - Version field: shrinkwrapVersion (number, typically 3 or 4)
|
|
7
|
+
* - Package key format: /name/version or /@scope/name/version
|
|
8
|
+
* - Peer dependency suffix: /peer@ver with ! escaping for scoped packages
|
|
9
|
+
* Example: /foo/1.0.0/bar@2.0.0+@scope!qar@3.0.0
|
|
10
|
+
*
|
|
11
|
+
* @module flatlock/parsers/pnpm/shrinkwrap
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @typedef {Object} ParsedSpec
|
|
16
|
+
* @property {string|null} name - The package name (null if unparseable)
|
|
17
|
+
* @property {string|null} version - The package version (null if unparseable)
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Parse a shrinkwrap.yaml package spec (v3/v4 format).
|
|
22
|
+
*
|
|
23
|
+
* Shrinkwrap format uses:
|
|
24
|
+
* - Slash separator between name and version: /name/version
|
|
25
|
+
* - Peer dependencies after another slash: /name/version/peer@ver
|
|
26
|
+
* - Scoped packages: /@scope/name/version
|
|
27
|
+
* - Scoped peer dependencies use `!` to escape the `@`: `/name/1.0.0/peer@2.0.0+@scope!qar@3.0.0`
|
|
28
|
+
*
|
|
29
|
+
* @param {string} spec - Package spec from shrinkwrap.yaml packages section
|
|
30
|
+
* @returns {ParsedSpec} Parsed name and version
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // Unscoped package
|
|
34
|
+
* parseSpecShrinkwrap('/lodash/4.17.21')
|
|
35
|
+
* // => { name: 'lodash', version: '4.17.21' }
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* // Scoped package
|
|
39
|
+
* parseSpecShrinkwrap('/@babel/core/7.23.0')
|
|
40
|
+
* // => { name: '@babel/core', version: '7.23.0' }
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // With peer dependency suffix
|
|
44
|
+
* parseSpecShrinkwrap('/foo/1.0.0/bar@2.0.0')
|
|
45
|
+
* // => { name: 'foo', version: '1.0.0' }
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // With scoped peer dependency (`!` escapes `@`)
|
|
49
|
+
* parseSpecShrinkwrap('/foo/1.0.0/bar@2.0.0+@scope!qar@3.0.0')
|
|
50
|
+
* // => { name: 'foo', version: '1.0.0' }
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Scoped package with peer deps
|
|
54
|
+
* parseSpecShrinkwrap('/@emotion/styled/10.0.27/react@17.0.2')
|
|
55
|
+
* // => { name: '@emotion/styled', version: '10.0.27' }
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Multiple peer dependencies
|
|
59
|
+
* parseSpecShrinkwrap('/styled-components/5.3.6/react-dom@17.0.2+react@17.0.2')
|
|
60
|
+
* // => { name: 'styled-components', version: '5.3.6' }
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // Package with hyphenated name
|
|
64
|
+
* parseSpecShrinkwrap('/string-width/4.2.3')
|
|
65
|
+
* // => { name: 'string-width', version: '4.2.3' }
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Scoped package with hyphenated name
|
|
69
|
+
* parseSpecShrinkwrap('/@babel/helper-compilation-targets/7.23.6')
|
|
70
|
+
* // => { name: '@babel/helper-compilation-targets', version: '7.23.6' }
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // link: protocol - skipped
|
|
74
|
+
* parseSpecShrinkwrap('link:packages/my-pkg')
|
|
75
|
+
* // => { name: null, version: null }
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // file: protocol - skipped
|
|
79
|
+
* parseSpecShrinkwrap('file:../local-package')
|
|
80
|
+
* // => { name: null, version: null }
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // Null input
|
|
84
|
+
* parseSpecShrinkwrap(null)
|
|
85
|
+
* // => { name: null, version: null }
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Empty string
|
|
89
|
+
* parseSpecShrinkwrap('')
|
|
90
|
+
* // => { name: null, version: null }
|
|
91
|
+
*/
|
|
92
|
+
export function parseSpecShrinkwrap(spec) {
|
|
93
|
+
// Handle null/undefined input
|
|
94
|
+
if (spec == null || typeof spec !== 'string') {
|
|
95
|
+
return { name: null, version: null };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Skip special protocols
|
|
99
|
+
if (spec.startsWith('link:') || spec.startsWith('file:')) {
|
|
100
|
+
return { name: null, version: null };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Remove leading slash if present
|
|
104
|
+
const cleaned = spec.startsWith('/') ? spec.slice(1) : spec;
|
|
105
|
+
|
|
106
|
+
// Handle empty string after removing slash
|
|
107
|
+
if (!cleaned) {
|
|
108
|
+
return { name: null, version: null };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Split by slash
|
|
112
|
+
const parts = cleaned.split('/');
|
|
113
|
+
|
|
114
|
+
// Determine if this is a scoped package
|
|
115
|
+
// Scoped packages start with @ and have format: @scope/name/version[/peer-suffix]
|
|
116
|
+
// Unscoped packages have format: name/version[/peer-suffix]
|
|
117
|
+
|
|
118
|
+
if (cleaned.startsWith('@')) {
|
|
119
|
+
// Scoped package: @scope/name/version[/peer-suffix]
|
|
120
|
+
// parts[0] = '@scope', parts[1] = 'name', parts[2] = 'version', parts[3+] = peer suffix
|
|
121
|
+
|
|
122
|
+
if (parts.length < 3) {
|
|
123
|
+
// Not enough parts for scoped package
|
|
124
|
+
return { name: null, version: null };
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const scope = parts[0]; // e.g., '@babel'
|
|
128
|
+
const pkgName = parts[1]; // e.g., 'core'
|
|
129
|
+
const version = parts[2]; // e.g., '7.23.0'
|
|
130
|
+
|
|
131
|
+
// Validate scope format
|
|
132
|
+
if (!scope.startsWith('@') || !scope.slice(1)) {
|
|
133
|
+
return { name: null, version: null };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Validate we have both name and version
|
|
137
|
+
if (!pkgName || !version) {
|
|
138
|
+
return { name: null, version: null };
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// The version might contain additional peer suffix parts that got split
|
|
142
|
+
// In shrinkwrap v3/v4, peer suffixes come after another slash
|
|
143
|
+
// But the version itself should be the semver string
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
name: `${scope}/${pkgName}`,
|
|
147
|
+
version: version
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Unscoped package: name/version[/peer-suffix]
|
|
152
|
+
// parts[0] = 'name', parts[1] = 'version', parts[2+] = peer suffix
|
|
153
|
+
|
|
154
|
+
if (parts.length < 2) {
|
|
155
|
+
// Not enough parts
|
|
156
|
+
return { name: null, version: null };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const name = parts[0];
|
|
160
|
+
const version = parts[1];
|
|
161
|
+
|
|
162
|
+
// Validate we have both name and version
|
|
163
|
+
if (!name || !version) {
|
|
164
|
+
return { name: null, version: null };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return { name, version };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Check if a spec has peer dependency suffix (shrinkwrap v3/v4 format).
|
|
172
|
+
*
|
|
173
|
+
* In shrinkwrap v3/v4, peer dependencies are appended after the version
|
|
174
|
+
* with another slash: /name/version/peer@ver+peer2@ver
|
|
175
|
+
*
|
|
176
|
+
* @param {string} spec - Package spec from shrinkwrap.yaml
|
|
177
|
+
* @returns {boolean} True if the spec has peer dependency suffix
|
|
178
|
+
*
|
|
179
|
+
* @example
|
|
180
|
+
* hasPeerSuffix('/lodash/4.17.21') // => false
|
|
181
|
+
* hasPeerSuffix('/foo/1.0.0/bar@2.0.0') // => true
|
|
182
|
+
* hasPeerSuffix('/@babel/core/7.23.0') // => false
|
|
183
|
+
* hasPeerSuffix('/@emotion/styled/10.0.27/react@17.0.2') // => true
|
|
184
|
+
*/
|
|
185
|
+
export function hasPeerSuffix(spec) {
|
|
186
|
+
if (spec == null || typeof spec !== 'string') {
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Remove leading slash
|
|
191
|
+
const cleaned = spec.startsWith('/') ? spec.slice(1) : spec;
|
|
192
|
+
|
|
193
|
+
// Count slashes
|
|
194
|
+
const slashCount = (cleaned.match(/\//g) || []).length;
|
|
195
|
+
|
|
196
|
+
// Scoped packages have 2+ slashes (scope/name/version), peer adds more
|
|
197
|
+
// Unscoped packages have 1+ slash (name/version), peer adds more
|
|
198
|
+
|
|
199
|
+
if (cleaned.startsWith('@')) {
|
|
200
|
+
// Scoped: needs > 2 slashes for peer suffix
|
|
201
|
+
return slashCount > 2;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Unscoped: needs > 1 slash for peer suffix
|
|
205
|
+
return slashCount > 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Extract the peer dependency suffix from a shrinkwrap spec.
|
|
210
|
+
*
|
|
211
|
+
* @param {string} spec - Package spec from shrinkwrap.yaml
|
|
212
|
+
* @returns {string|null} The peer suffix or null if none
|
|
213
|
+
*
|
|
214
|
+
* @example
|
|
215
|
+
* extractPeerSuffix('/lodash/4.17.21') // => null
|
|
216
|
+
* extractPeerSuffix('/foo/1.0.0/bar@2.0.0') // => 'bar@2.0.0'
|
|
217
|
+
* extractPeerSuffix('/foo/1.0.0/bar@2.0.0+@scope!qar@3.0.0') // => 'bar@2.0.0+@scope!qar@3.0.0'
|
|
218
|
+
*/
|
|
219
|
+
export function extractPeerSuffix(spec) {
|
|
220
|
+
if (spec == null || typeof spec !== 'string') {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
// Remove leading slash
|
|
225
|
+
const cleaned = spec.startsWith('/') ? spec.slice(1) : spec;
|
|
226
|
+
const parts = cleaned.split('/');
|
|
227
|
+
|
|
228
|
+
if (cleaned.startsWith('@')) {
|
|
229
|
+
// Scoped: @scope/name/version[/peer-suffix...]
|
|
230
|
+
if (parts.length <= 3) {
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
return parts.slice(3).join('/');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Unscoped: name/version[/peer-suffix...]
|
|
237
|
+
if (parts.length <= 2) {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
return parts.slice(2).join('/');
|
|
241
|
+
}
|