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.
Files changed (49) hide show
  1. package/README.md +54 -1
  2. package/bin/flatlock-cmp.js +71 -45
  3. package/dist/compare.d.ts +25 -3
  4. package/dist/compare.d.ts.map +1 -1
  5. package/dist/detect.d.ts.map +1 -1
  6. package/dist/index.d.ts +3 -1
  7. package/dist/index.d.ts.map +1 -1
  8. package/dist/parsers/index.d.ts +2 -2
  9. package/dist/parsers/npm.d.ts +64 -37
  10. package/dist/parsers/npm.d.ts.map +1 -1
  11. package/dist/parsers/pnpm/detect.d.ts +136 -0
  12. package/dist/parsers/pnpm/detect.d.ts.map +1 -0
  13. package/dist/parsers/pnpm/index.d.ts +120 -0
  14. package/dist/parsers/pnpm/index.d.ts.map +1 -0
  15. package/dist/parsers/pnpm/internal.d.ts +5 -0
  16. package/dist/parsers/pnpm/internal.d.ts.map +1 -0
  17. package/dist/parsers/pnpm/shrinkwrap.d.ts +129 -0
  18. package/dist/parsers/pnpm/shrinkwrap.d.ts.map +1 -0
  19. package/dist/parsers/pnpm/v5.d.ts +139 -0
  20. package/dist/parsers/pnpm/v5.d.ts.map +1 -0
  21. package/dist/parsers/pnpm/v6plus.d.ts +212 -0
  22. package/dist/parsers/pnpm/v6plus.d.ts.map +1 -0
  23. package/dist/parsers/pnpm.d.ts +1 -59
  24. package/dist/parsers/pnpm.d.ts.map +1 -1
  25. package/dist/parsers/types.d.ts +23 -0
  26. package/dist/parsers/types.d.ts.map +1 -0
  27. package/dist/parsers/yarn-berry.d.ts +141 -52
  28. package/dist/parsers/yarn-berry.d.ts.map +1 -1
  29. package/dist/parsers/yarn-classic.d.ts +79 -33
  30. package/dist/parsers/yarn-classic.d.ts.map +1 -1
  31. package/dist/set.d.ts +189 -0
  32. package/dist/set.d.ts.map +1 -0
  33. package/package.json +7 -5
  34. package/src/compare.js +385 -28
  35. package/src/detect.js +3 -4
  36. package/src/index.js +9 -2
  37. package/src/parsers/index.js +10 -2
  38. package/src/parsers/npm.js +64 -16
  39. package/src/parsers/pnpm/detect.js +198 -0
  40. package/src/parsers/pnpm/index.js +289 -0
  41. package/src/parsers/pnpm/internal.js +41 -0
  42. package/src/parsers/pnpm/shrinkwrap.js +241 -0
  43. package/src/parsers/pnpm/v5.js +225 -0
  44. package/src/parsers/pnpm/v6plus.js +290 -0
  45. package/src/parsers/pnpm.js +11 -89
  46. package/src/parsers/types.js +10 -0
  47. package/src/parsers/yarn-berry.js +183 -36
  48. package/src/parsers/yarn-classic.js +81 -21
  49. package/src/set.js +618 -0
@@ -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} content - Lockfile content
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(content, _options = {}) {
69
- const lockfile = JSON.parse(content);
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';