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
@@ -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
+ }
@@ -0,0 +1,225 @@
1
+ /**
2
+ * @fileoverview Parser for pnpm-lock.yaml v5.x format
3
+ *
4
+ * pnpm-lock.yaml v5.x format (2019-2022) characteristics:
5
+ * - File: pnpm-lock.yaml
6
+ * - Version field: lockfileVersion (number like 5, 5.1, 5.2, 5.3, 5.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
+ * Key differences from shrinkwrap v3/v4:
12
+ * - Peer suffix uses _ instead of /
13
+ * - Scoped peer packages escape @ with + instead of !
14
+ *
15
+ * @module flatlock/parsers/pnpm/v5
16
+ */
17
+
18
+ /**
19
+ * @typedef {Object} ParsedSpec
20
+ * @property {string|null} name - The package name (null if unparseable)
21
+ * @property {string|null} version - The package version (null if unparseable)
22
+ */
23
+
24
+ /**
25
+ * Parse a pnpm-lock.yaml v5.x package spec.
26
+ *
27
+ * v5 format uses:
28
+ * - Slash separator between name and version: /name/version
29
+ * - Peer dependencies after underscore: /name/version_peer@ver
30
+ * - Scoped packages: /@scope/name/version
31
+ * - Multiple peers joined with +: /name/1.0.0_peer1@2.0.0+peer2@3.0.0
32
+ * - Scoped peer dependencies use + to escape the /: _@scope+pkg@1.0.0
33
+ *
34
+ * @param {string} spec - Package spec from pnpm-lock.yaml packages section
35
+ * @returns {ParsedSpec} Parsed name and version
36
+ *
37
+ * @example
38
+ * // Unscoped package
39
+ * parseSpecV5('/lodash/4.17.21')
40
+ * // => { name: 'lodash', version: '4.17.21' }
41
+ *
42
+ * @example
43
+ * // Scoped package
44
+ * parseSpecV5('/@babel/core/7.23.0')
45
+ * // => { name: '@babel/core', version: '7.23.0' }
46
+ *
47
+ * @example
48
+ * // With peer dependency suffix
49
+ * parseSpecV5('/styled-jsx/3.0.9_react@17.0.2')
50
+ * // => { name: 'styled-jsx', version: '3.0.9' }
51
+ *
52
+ * @example
53
+ * // With multiple peer dependencies
54
+ * parseSpecV5('/pkg/1.0.0_react-dom@17.0.2+react@17.0.2')
55
+ * // => { name: 'pkg', version: '1.0.0' }
56
+ *
57
+ * @example
58
+ * // Scoped package with peer deps
59
+ * parseSpecV5('/@emotion/styled/10.0.27_react@17.0.2')
60
+ * // => { name: '@emotion/styled', version: '10.0.27' }
61
+ *
62
+ * @example
63
+ * // Prerelease version
64
+ * parseSpecV5('/@verdaccio/ui-theme/6.0.0-6-next.50')
65
+ * // => { name: '@verdaccio/ui-theme', version: '6.0.0-6-next.50' }
66
+ *
67
+ * @example
68
+ * // Package with hyphenated name
69
+ * parseSpecV5('/string-width/4.2.3')
70
+ * // => { name: 'string-width', version: '4.2.3' }
71
+ *
72
+ * @example
73
+ * // Scoped package with hyphenated name
74
+ * parseSpecV5('/@babel/helper-compilation-targets/7.23.6')
75
+ * // => { name: '@babel/helper-compilation-targets', version: '7.23.6' }
76
+ *
77
+ * @example
78
+ * // Complex peer suffix with scoped peer
79
+ * parseSpecV5('/styled-components/5.3.6_@babel+core@7.23.0+react@18.2.0')
80
+ * // => { name: 'styled-components', version: '5.3.6' }
81
+ *
82
+ * @example
83
+ * // link: protocol - skipped
84
+ * parseSpecV5('link:packages/my-pkg')
85
+ * // => { name: null, version: null }
86
+ *
87
+ * @example
88
+ * // file: protocol - skipped
89
+ * parseSpecV5('file:../local-package')
90
+ * // => { name: null, version: null }
91
+ *
92
+ * @example
93
+ * // Null input
94
+ * parseSpecV5(null)
95
+ * // => { name: null, version: null }
96
+ *
97
+ * @example
98
+ * // Build metadata version
99
+ * parseSpecV5('/esbuild/0.19.12+sha512.abc123')
100
+ * // => { name: 'esbuild', version: '0.19.12+sha512.abc123' }
101
+ */
102
+ export function parseSpecV5(spec) {
103
+ // Handle null/undefined input
104
+ if (spec == null || typeof spec !== 'string') {
105
+ return { name: null, version: null };
106
+ }
107
+
108
+ // Skip special protocols
109
+ if (spec.startsWith('link:') || spec.startsWith('file:')) {
110
+ return { name: null, version: null };
111
+ }
112
+
113
+ // Remove leading slash if present
114
+ let cleaned = spec.startsWith('/') ? spec.slice(1) : spec;
115
+
116
+ // Handle empty string after removing slash
117
+ if (!cleaned) {
118
+ return { name: null, version: null };
119
+ }
120
+
121
+ // Strip peer dependency suffix FIRST (before splitting)
122
+ // v5 format uses underscore: pkg/1.0.0_peer@2.0.0+other@1.0.0
123
+ const underscoreIndex = cleaned.indexOf('_');
124
+ if (underscoreIndex !== -1) {
125
+ cleaned = cleaned.slice(0, underscoreIndex);
126
+ }
127
+
128
+ // Now split by slash to get name and version parts
129
+ const parts = cleaned.split('/');
130
+
131
+ // Determine if this is a scoped package
132
+ if (cleaned.startsWith('@')) {
133
+ // Scoped package: @scope/name/version
134
+ // parts[0] = '@scope', parts[1] = 'name', parts[2] = 'version'
135
+
136
+ if (parts.length < 3) {
137
+ // Not enough parts for scoped package
138
+ return { name: null, version: null };
139
+ }
140
+
141
+ const scope = parts[0]; // e.g., '@babel'
142
+ const pkgName = parts[1]; // e.g., 'core'
143
+ const version = parts[2]; // e.g., '7.23.0'
144
+
145
+ // Validate scope format
146
+ if (!scope.startsWith('@') || !scope.slice(1)) {
147
+ return { name: null, version: null };
148
+ }
149
+
150
+ // Validate we have both name and version
151
+ if (!pkgName || !version) {
152
+ return { name: null, version: null };
153
+ }
154
+
155
+ return {
156
+ name: `${scope}/${pkgName}`,
157
+ version: version
158
+ };
159
+ }
160
+
161
+ // Unscoped package: name/version
162
+ // parts[0] = 'name', parts[1] = 'version'
163
+
164
+ if (parts.length < 2) {
165
+ // Not enough parts
166
+ return { name: null, version: null };
167
+ }
168
+
169
+ const name = parts[0];
170
+ const version = parts[1];
171
+
172
+ // Validate we have both name and version
173
+ if (!name || !version) {
174
+ return { name: null, version: null };
175
+ }
176
+
177
+ return { name, version };
178
+ }
179
+
180
+ /**
181
+ * Check if a spec has peer dependency suffix (v5 format).
182
+ *
183
+ * In v5, peer dependencies are appended after the version
184
+ * with an underscore: /name/version_peer@ver+peer2@ver
185
+ *
186
+ * @param {string} spec - Package spec from pnpm-lock.yaml
187
+ * @returns {boolean} True if the spec has peer dependency suffix
188
+ *
189
+ * @example
190
+ * hasPeerSuffixV5('/lodash/4.17.21') // => false
191
+ * hasPeerSuffixV5('/foo/1.0.0_bar@2.0.0') // => true
192
+ * hasPeerSuffixV5('/@babel/core/7.23.0') // => false
193
+ * hasPeerSuffixV5('/@emotion/styled/10.0.27_react@17.0.2') // => true
194
+ */
195
+ export function hasPeerSuffixV5(spec) {
196
+ if (spec == null || typeof spec !== 'string') {
197
+ return false;
198
+ }
199
+
200
+ return spec.includes('_');
201
+ }
202
+
203
+ /**
204
+ * Extract the peer dependency suffix from a v5 spec.
205
+ *
206
+ * @param {string} spec - Package spec from pnpm-lock.yaml v5
207
+ * @returns {string|null} The peer suffix or null if none
208
+ *
209
+ * @example
210
+ * extractPeerSuffixV5('/lodash/4.17.21') // => null
211
+ * extractPeerSuffixV5('/foo/1.0.0_bar@2.0.0') // => 'bar@2.0.0'
212
+ * extractPeerSuffixV5('/foo/1.0.0_bar@2.0.0+@scope+qar@3.0.0') // => 'bar@2.0.0+@scope+qar@3.0.0'
213
+ */
214
+ export function extractPeerSuffixV5(spec) {
215
+ if (spec == null || typeof spec !== 'string') {
216
+ return null;
217
+ }
218
+
219
+ const underscoreIndex = spec.indexOf('_');
220
+ if (underscoreIndex === -1) {
221
+ return null;
222
+ }
223
+
224
+ return spec.slice(underscoreIndex + 1);
225
+ }