flatlock 1.1.0 → 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 +54 -1
- package/bin/flatlock-cmp.js +71 -45
- package/dist/compare.d.ts +25 -3
- package/dist/compare.d.ts.map +1 -1
- package/dist/detect.d.ts.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/parsers/index.d.ts +2 -2
- package/dist/parsers/npm.d.ts +64 -37
- package/dist/parsers/npm.d.ts.map +1 -1
- 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 +1 -59
- package/dist/parsers/pnpm.d.ts.map +1 -1
- package/dist/parsers/types.d.ts +23 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/yarn-berry.d.ts +141 -52
- package/dist/parsers/yarn-berry.d.ts.map +1 -1
- package/dist/parsers/yarn-classic.d.ts +79 -33
- package/dist/parsers/yarn-classic.d.ts.map +1 -1
- package/dist/set.d.ts +189 -0
- package/dist/set.d.ts.map +1 -0
- package/package.json +7 -5
- package/src/compare.js +385 -28
- package/src/detect.js +3 -4
- package/src/index.js +9 -2
- package/src/parsers/index.js +10 -2
- package/src/parsers/npm.js +64 -16
- 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 +11 -89
- package/src/parsers/types.js +10 -0
- package/src/parsers/yarn-berry.js +183 -36
- package/src/parsers/yarn-classic.js +81 -21
- package/src/set.js +618 -0
package/src/parsers/npm.js
CHANGED
|
@@ -1,11 +1,4 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @typedef {Object} Dependency
|
|
3
|
-
* @property {string} name - Package name
|
|
4
|
-
* @property {string} version - Resolved version
|
|
5
|
-
* @property {string} [integrity] - Integrity hash
|
|
6
|
-
* @property {string} [resolved] - Resolution URL
|
|
7
|
-
* @property {boolean} [link] - True if this is a symlink
|
|
8
|
-
*/
|
|
1
|
+
/** @typedef {import('./types.js').Dependency} Dependency */
|
|
9
2
|
|
|
10
3
|
/**
|
|
11
4
|
* LIMITATION: Workspace symlinks are not yielded
|
|
@@ -43,13 +36,68 @@
|
|
|
43
36
|
* pkg := name (unscoped)
|
|
44
37
|
* | @scope/name (scoped)
|
|
45
38
|
*
|
|
46
|
-
* Examples:
|
|
47
|
-
* - node_modules/lodash → "lodash"
|
|
48
|
-
* - node_modules/@babel/core → "@babel/core"
|
|
49
|
-
* - node_modules/foo/node_modules/@scope/bar → "@scope/bar"
|
|
50
|
-
*
|
|
51
39
|
* @param {string} path - Lockfile path key
|
|
52
40
|
* @returns {string} Package name
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* // Simple unscoped package
|
|
44
|
+
* parseLockfileKey('node_modules/lodash')
|
|
45
|
+
* // => 'lodash'
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* // Scoped package
|
|
49
|
+
* parseLockfileKey('node_modules/@babel/core')
|
|
50
|
+
* // => '@babel/core'
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* // Nested dependency (hoisted conflict resolution)
|
|
54
|
+
* parseLockfileKey('node_modules/foo/node_modules/bar')
|
|
55
|
+
* // => 'bar'
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* // Nested scoped dependency
|
|
59
|
+
* parseLockfileKey('node_modules/foo/node_modules/@scope/bar')
|
|
60
|
+
* // => '@scope/bar'
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* // Deeply nested dependency
|
|
64
|
+
* parseLockfileKey('node_modules/a/node_modules/b/node_modules/c')
|
|
65
|
+
* // => 'c'
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* // Deeply nested scoped dependency
|
|
69
|
+
* parseLockfileKey('node_modules/a/node_modules/@types/node')
|
|
70
|
+
* // => '@types/node'
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* // Workspace package path (definition)
|
|
74
|
+
* parseLockfileKey('packages/my-lib')
|
|
75
|
+
* // => 'my-lib'
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* // Workspace nested dependency
|
|
79
|
+
* parseLockfileKey('packages/my-lib/node_modules/lodash')
|
|
80
|
+
* // => 'lodash'
|
|
81
|
+
*
|
|
82
|
+
* @example
|
|
83
|
+
* // Workspace nested scoped dependency
|
|
84
|
+
* parseLockfileKey('packages/my-lib/node_modules/@types/react')
|
|
85
|
+
* // => '@types/react'
|
|
86
|
+
*
|
|
87
|
+
* @example
|
|
88
|
+
* // Package with hyphenated name
|
|
89
|
+
* parseLockfileKey('node_modules/string-width')
|
|
90
|
+
* // => 'string-width'
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* // Scoped package with hyphenated name
|
|
94
|
+
* parseLockfileKey('node_modules/@emotion/styled')
|
|
95
|
+
* // => '@emotion/styled'
|
|
96
|
+
*
|
|
97
|
+
* @example
|
|
98
|
+
* // Complex nested path
|
|
99
|
+
* parseLockfileKey('node_modules/@babel/core/node_modules/@babel/helper-compilation-targets')
|
|
100
|
+
* // => '@babel/helper-compilation-targets'
|
|
53
101
|
*/
|
|
54
102
|
export function parseLockfileKey(path) {
|
|
55
103
|
const parts = path.split('/');
|
|
@@ -61,12 +109,12 @@ export function parseLockfileKey(path) {
|
|
|
61
109
|
|
|
62
110
|
/**
|
|
63
111
|
* Parse npm package-lock.json (v1, v2, v3)
|
|
64
|
-
* @param {string}
|
|
112
|
+
* @param {string | object} input - Lockfile content string or pre-parsed object
|
|
65
113
|
* @param {Object} [_options] - Parser options (unused, reserved for future use)
|
|
66
114
|
* @returns {Generator<Dependency>}
|
|
67
115
|
*/
|
|
68
|
-
export function* fromPackageLock(
|
|
69
|
-
const lockfile = JSON.parse(
|
|
116
|
+
export function* fromPackageLock(input, _options = {}) {
|
|
117
|
+
const lockfile = typeof input === 'string' ? JSON.parse(input) : input;
|
|
70
118
|
const packages = lockfile.packages || {};
|
|
71
119
|
|
|
72
120
|
for (const [path, pkg] of Object.entries(packages)) {
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Version detection for pnpm lockfiles
|
|
3
|
+
*
|
|
4
|
+
* Detects the era and version of pnpm lockfiles including:
|
|
5
|
+
* - shrinkwrap.yaml (v3/v4) from 2016-2019
|
|
6
|
+
* - pnpm-lock.yaml v5.x (2019-2022)
|
|
7
|
+
* - pnpm-lock.yaml v6.0 (2023)
|
|
8
|
+
* - pnpm-lock.yaml v9.0 (2024+)
|
|
9
|
+
*
|
|
10
|
+
* @module flatlock/parsers/pnpm/detect
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @typedef {'shrinkwrap'|'v5'|'v5-inline'|'v6'|'v9'|'unknown'} LockfileEra
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @typedef {Object} DetectedVersion
|
|
19
|
+
* @property {LockfileEra} era - The lockfile era/format family
|
|
20
|
+
* @property {string|number} version - The raw version from the lockfile
|
|
21
|
+
* @property {boolean} isShrinkwrap - True if this is a shrinkwrap.yaml file
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Detect the version and era of a pnpm lockfile.
|
|
26
|
+
*
|
|
27
|
+
* Version detection rules:
|
|
28
|
+
* - If `shrinkwrapVersion` exists: shrinkwrap era (v3/v4)
|
|
29
|
+
* - If `lockfileVersion` is a number: v5 era
|
|
30
|
+
* - If `lockfileVersion` is '5.4-inlineSpecifiers': v5-inline era
|
|
31
|
+
* - If `lockfileVersion` starts with '6': v6 era
|
|
32
|
+
* - If `lockfileVersion` starts with '9': v9 era
|
|
33
|
+
*
|
|
34
|
+
* @param {Object} lockfile - Parsed pnpm lockfile object
|
|
35
|
+
* @param {string|number} [lockfile.lockfileVersion] - The lockfile version field
|
|
36
|
+
* @param {number} [lockfile.shrinkwrapVersion] - The shrinkwrap version field (v3/v4)
|
|
37
|
+
* @param {number} [lockfile.shrinkwrapMinorVersion] - Minor version for shrinkwrap
|
|
38
|
+
* @returns {DetectedVersion} The detected version information
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* // shrinkwrap.yaml v3
|
|
42
|
+
* detectVersion({ shrinkwrapVersion: 3 })
|
|
43
|
+
* // => { era: 'shrinkwrap', version: 3, isShrinkwrap: true }
|
|
44
|
+
*
|
|
45
|
+
* @example
|
|
46
|
+
* // pnpm-lock.yaml v5.4
|
|
47
|
+
* detectVersion({ lockfileVersion: 5.4 })
|
|
48
|
+
* // => { era: 'v5', version: 5.4, isShrinkwrap: false }
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* // pnpm-lock.yaml v6.0
|
|
52
|
+
* detectVersion({ lockfileVersion: '6.0' })
|
|
53
|
+
* // => { era: 'v6', version: '6.0', isShrinkwrap: false }
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // pnpm-lock.yaml v9.0
|
|
57
|
+
* detectVersion({ lockfileVersion: '9.0' })
|
|
58
|
+
* // => { era: 'v9', version: '9.0', isShrinkwrap: false }
|
|
59
|
+
*/
|
|
60
|
+
export function detectVersion(lockfile) {
|
|
61
|
+
// Handle null/undefined input
|
|
62
|
+
if (!lockfile || typeof lockfile !== 'object') {
|
|
63
|
+
return { era: 'unknown', version: '', isShrinkwrap: false };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Check for shrinkwrap.yaml (v3/v4) - oldest format
|
|
67
|
+
if ('shrinkwrapVersion' in lockfile) {
|
|
68
|
+
const version = lockfile.shrinkwrapVersion;
|
|
69
|
+
return {
|
|
70
|
+
era: 'shrinkwrap',
|
|
71
|
+
version: version ?? 0,
|
|
72
|
+
isShrinkwrap: true
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Get the lockfileVersion
|
|
77
|
+
const version = lockfile.lockfileVersion;
|
|
78
|
+
|
|
79
|
+
// Handle missing version
|
|
80
|
+
if (version === undefined || version === null) {
|
|
81
|
+
return { era: 'unknown', version: '', isShrinkwrap: false };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Numeric version: v5.x era
|
|
85
|
+
if (typeof version === 'number') {
|
|
86
|
+
return {
|
|
87
|
+
era: 'v5',
|
|
88
|
+
version: version,
|
|
89
|
+
isShrinkwrap: false
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// String version
|
|
94
|
+
if (typeof version === 'string') {
|
|
95
|
+
// v5.4-inlineSpecifiers: experimental transitional format
|
|
96
|
+
if (version.includes('inlineSpecifiers')) {
|
|
97
|
+
return {
|
|
98
|
+
era: 'v5-inline',
|
|
99
|
+
version: version,
|
|
100
|
+
isShrinkwrap: false
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// v9.x era (2024+)
|
|
105
|
+
if (version.startsWith('9')) {
|
|
106
|
+
return {
|
|
107
|
+
era: 'v9',
|
|
108
|
+
version: version,
|
|
109
|
+
isShrinkwrap: false
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// v6.x era (2023)
|
|
114
|
+
if (version.startsWith('6')) {
|
|
115
|
+
return {
|
|
116
|
+
era: 'v6',
|
|
117
|
+
version: version,
|
|
118
|
+
isShrinkwrap: false
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// v7.x or v8.x would fall here if they existed (they don't)
|
|
123
|
+
// Could be a future version we don't know about
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return { era: 'unknown', version: version, isShrinkwrap: false };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Check if a lockfile uses the v6+ package key format (name@version).
|
|
131
|
+
*
|
|
132
|
+
* v5 and earlier use: /name/version or /@scope/name/version
|
|
133
|
+
* v6+ use: /name@version or /@scope/name@version
|
|
134
|
+
* v9+ use: name@version (no leading slash)
|
|
135
|
+
*
|
|
136
|
+
* @param {DetectedVersion} detected - The detected version info
|
|
137
|
+
* @returns {boolean} True if the lockfile uses @ separator for name@version
|
|
138
|
+
*
|
|
139
|
+
* @example
|
|
140
|
+
* usesAtSeparator({ era: 'v5', version: 5.4 }) // => false
|
|
141
|
+
* usesAtSeparator({ era: 'v6', version: '6.0' }) // => true
|
|
142
|
+
* usesAtSeparator({ era: 'v9', version: '9.0' }) // => true
|
|
143
|
+
*/
|
|
144
|
+
export function usesAtSeparator(detected) {
|
|
145
|
+
return detected.era === 'v6' || detected.era === 'v9';
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Check if a lockfile uses the packages/snapshots split (v9+).
|
|
150
|
+
*
|
|
151
|
+
* v9 separates package metadata (packages) from dependency relationships (snapshots).
|
|
152
|
+
*
|
|
153
|
+
* @param {DetectedVersion} detected - The detected version info
|
|
154
|
+
* @returns {boolean} True if the lockfile has packages/snapshots split
|
|
155
|
+
*
|
|
156
|
+
* @example
|
|
157
|
+
* usesSnapshotsSplit({ era: 'v6', version: '6.0' }) // => false
|
|
158
|
+
* usesSnapshotsSplit({ era: 'v9', version: '9.0' }) // => true
|
|
159
|
+
*/
|
|
160
|
+
export function usesSnapshotsSplit(detected) {
|
|
161
|
+
return detected.era === 'v9';
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if a lockfile uses inline specifiers.
|
|
166
|
+
*
|
|
167
|
+
* v5.4-inlineSpecifiers and v6+ use inline specifiers in importers.
|
|
168
|
+
* Earlier versions have a separate `specifiers` block.
|
|
169
|
+
*
|
|
170
|
+
* @param {DetectedVersion} detected - The detected version info
|
|
171
|
+
* @returns {boolean} True if specifiers are inlined
|
|
172
|
+
*
|
|
173
|
+
* @example
|
|
174
|
+
* usesInlineSpecifiers({ era: 'v5', version: 5.4 }) // => false
|
|
175
|
+
* usesInlineSpecifiers({ era: 'v5-inline', version: '5.4-inlineSpecifiers' }) // => true
|
|
176
|
+
* usesInlineSpecifiers({ era: 'v6', version: '6.0' }) // => true
|
|
177
|
+
*/
|
|
178
|
+
export function usesInlineSpecifiers(detected) {
|
|
179
|
+
return detected.era === 'v5-inline' || detected.era === 'v6' || detected.era === 'v9';
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Check if package keys have a leading slash.
|
|
184
|
+
*
|
|
185
|
+
* v5 and v6 use leading slash: /name/version or /name@version
|
|
186
|
+
* v9 omits leading slash: name@version
|
|
187
|
+
*
|
|
188
|
+
* @param {DetectedVersion} detected - The detected version info
|
|
189
|
+
* @returns {boolean} True if package keys have leading slash
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* hasLeadingSlash({ era: 'v5', version: 5.4 }) // => true
|
|
193
|
+
* hasLeadingSlash({ era: 'v6', version: '6.0' }) // => true
|
|
194
|
+
* hasLeadingSlash({ era: 'v9', version: '9.0' }) // => false
|
|
195
|
+
*/
|
|
196
|
+
export function hasLeadingSlash(detected) {
|
|
197
|
+
return detected.era !== 'v9';
|
|
198
|
+
}
|
|
@@ -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';
|