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.
Files changed (55) hide show
  1. package/README.md +55 -2
  2. package/bin/flatlock-cmp.js +109 -356
  3. package/dist/compare.d.ts +85 -0
  4. package/dist/compare.d.ts.map +1 -0
  5. package/dist/detect.d.ts +33 -0
  6. package/dist/detect.d.ts.map +1 -0
  7. package/dist/index.d.ts +60 -20
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/parsers/index.d.ts +5 -0
  10. package/dist/parsers/index.d.ts.map +1 -0
  11. package/dist/parsers/npm.d.ts +109 -0
  12. package/dist/parsers/npm.d.ts.map +1 -0
  13. package/dist/parsers/pnpm/detect.d.ts +136 -0
  14. package/dist/parsers/pnpm/detect.d.ts.map +1 -0
  15. package/dist/parsers/pnpm/index.d.ts +120 -0
  16. package/dist/parsers/pnpm/index.d.ts.map +1 -0
  17. package/dist/parsers/pnpm/internal.d.ts +5 -0
  18. package/dist/parsers/pnpm/internal.d.ts.map +1 -0
  19. package/dist/parsers/pnpm/shrinkwrap.d.ts +129 -0
  20. package/dist/parsers/pnpm/shrinkwrap.d.ts.map +1 -0
  21. package/dist/parsers/pnpm/v5.d.ts +139 -0
  22. package/dist/parsers/pnpm/v5.d.ts.map +1 -0
  23. package/dist/parsers/pnpm/v6plus.d.ts +212 -0
  24. package/dist/parsers/pnpm/v6plus.d.ts.map +1 -0
  25. package/dist/parsers/pnpm.d.ts +2 -0
  26. package/dist/parsers/pnpm.d.ts.map +1 -0
  27. package/dist/parsers/types.d.ts +23 -0
  28. package/dist/parsers/types.d.ts.map +1 -0
  29. package/dist/parsers/yarn-berry.d.ts +154 -0
  30. package/dist/parsers/yarn-berry.d.ts.map +1 -0
  31. package/dist/parsers/yarn-classic.d.ts +110 -0
  32. package/dist/parsers/yarn-classic.d.ts.map +1 -0
  33. package/dist/result.d.ts +12 -0
  34. package/dist/result.d.ts.map +1 -0
  35. package/dist/set.d.ts +189 -0
  36. package/dist/set.d.ts.map +1 -0
  37. package/package.json +18 -7
  38. package/src/compare.js +620 -0
  39. package/src/detect.js +8 -7
  40. package/src/index.js +33 -15
  41. package/src/parsers/index.js +12 -4
  42. package/src/parsers/npm.js +70 -23
  43. package/src/parsers/pnpm/detect.js +198 -0
  44. package/src/parsers/pnpm/index.js +289 -0
  45. package/src/parsers/pnpm/internal.js +41 -0
  46. package/src/parsers/pnpm/shrinkwrap.js +241 -0
  47. package/src/parsers/pnpm/v5.js +225 -0
  48. package/src/parsers/pnpm/v6plus.js +290 -0
  49. package/src/parsers/pnpm.js +12 -77
  50. package/src/parsers/types.js +10 -0
  51. package/src/parsers/yarn-berry.js +187 -38
  52. package/src/parsers/yarn-classic.js +85 -24
  53. package/src/result.js +2 -2
  54. package/src/set.js +618 -0
  55. package/src/types.d.ts +54 -0
@@ -1,41 +1,180 @@
1
1
  import { parseSyml } from '@yarnpkg/parsers';
2
2
 
3
+ /** @typedef {import('./types.js').Dependency} Dependency */
4
+
3
5
  /**
4
- * @typedef {Object} Dependency
5
- * @property {string} name - Package name
6
- * @property {string} version - Resolved version
7
- * @property {string} [integrity] - Integrity hash
8
- * @property {string} [resolved] - Resolution URL
9
- * @property {boolean} [link] - True if this is a symlink
6
+ * Extract package name from yarn berry resolution field.
7
+ *
8
+ * The resolution field is the CANONICAL identifier and should be used instead of the key.
9
+ * Keys can contain npm aliases (e.g., "string-width-cjs@npm:string-width@^4.2.0") while
10
+ * the resolution always contains the actual package name (e.g., "string-width@npm:4.2.3").
11
+ *
12
+ * @param {string} resolution - Resolution field from lockfile entry
13
+ * @returns {string | null} Package name or null if parsing fails
14
+ *
15
+ * @example
16
+ * // Unscoped npm package
17
+ * parseResolution('lodash@npm:4.17.21')
18
+ * // => 'lodash'
19
+ *
20
+ * @example
21
+ * // Scoped npm package
22
+ * parseResolution('@babel/core@npm:7.24.0')
23
+ * // => '@babel/core'
24
+ *
25
+ * @example
26
+ * // Aliased package - resolution shows the REAL package name
27
+ * // (key was "string-width-cjs@npm:string-width@^4.2.0")
28
+ * parseResolution('string-width@npm:4.2.3')
29
+ * // => 'string-width'
30
+ *
31
+ * @example
32
+ * // Scoped aliased package - resolution shows the REAL package name
33
+ * // (key was "@babel-baseline/core@npm:@babel/core@7.24.4")
34
+ * parseResolution('@babel/core@npm:7.24.4')
35
+ * // => '@babel/core'
36
+ *
37
+ * @example
38
+ * // Patch protocol (nested protocols)
39
+ * parseResolution('pkg@patch:pkg@npm:1.0.0#./patch')
40
+ * // => 'pkg'
41
+ *
42
+ * @example
43
+ * // Scoped package with patch protocol
44
+ * parseResolution('@scope/pkg@patch:@scope/pkg@npm:1.0.0#./fix.patch')
45
+ * // => '@scope/pkg'
46
+ *
47
+ * @example
48
+ * // Workspace protocol
49
+ * parseResolution('my-pkg@workspace:packages/my-pkg')
50
+ * // => 'my-pkg'
51
+ *
52
+ * @example
53
+ * // Scoped workspace package
54
+ * parseResolution('@myorg/utils@workspace:packages/utils')
55
+ * // => '@myorg/utils'
56
+ *
57
+ * @example
58
+ * // Git protocol
59
+ * parseResolution('my-lib@git:github.com/user/repo#commit-hash')
60
+ * // => 'my-lib'
61
+ *
62
+ * @example
63
+ * // Null/empty input
64
+ * parseResolution(null)
65
+ * // => null
66
+ *
67
+ * @example
68
+ * // Empty string
69
+ * parseResolution('')
70
+ * // => null
71
+ *
72
+ * @example
73
+ * // Portal protocol (symlink to external package)
74
+ * parseResolution('@scope/external@portal:../external-pkg')
75
+ * // => '@scope/external'
10
76
  */
77
+ export function parseResolution(resolution) {
78
+ if (!resolution) return null;
79
+
80
+ // Resolution format: name@protocol:version or @scope/name@protocol:version
81
+ // Examples:
82
+ // "lodash@npm:4.17.21"
83
+ // "@babel/core@npm:7.24.0"
84
+ // "pkg@patch:pkg@npm:1.0.0#./patch"
85
+
86
+ // Handle scoped packages: @scope/name@protocol:version
87
+ if (resolution.startsWith('@')) {
88
+ const slashIndex = resolution.indexOf('/');
89
+ if (slashIndex !== -1) {
90
+ // Find the @ after the scope/name
91
+ const afterSlash = resolution.indexOf('@', slashIndex);
92
+ if (afterSlash !== -1) {
93
+ return resolution.slice(0, afterSlash);
94
+ }
95
+ }
96
+ }
97
+
98
+ // Handle unscoped packages: name@protocol:version
99
+ const atIndex = resolution.indexOf('@');
100
+ if (atIndex !== -1) {
101
+ return resolution.slice(0, atIndex);
102
+ }
103
+
104
+ return null;
105
+ }
11
106
 
12
107
  /**
13
- * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
14
- * !! WARNING: DO NOT MODIFY THIS FUNCTION !!
15
- * !! AI AGENTS (Claude, Copilot, etc.): DO NOT TOUCH THIS CODE !!
16
- * !! !!
17
- * !! This is YARN BERRY LOCKFILE KEY parsing, NOT npm spec parsing. !!
18
- * !! Yarn berry lockfile keys have their own format: !!
19
- * !! - Protocol markers: @npm:, @workspace:, @patch:, @portal:, @link: !!
20
- * !! - Nested protocols: @patch:pkg@npm:version#hash !!
21
- * !! - Multiple comma-separated entries !!
22
- * !! !!
23
- * !! npm-package-arg (npa) does NOT understand these formats. !!
24
- * !! Do not "improve" this with npa. !!
25
- * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
26
- *
27
- * Extract package name from yarn berry key.
28
- *
29
- * Examples:
30
- * "lodash@npm:^4.17.21" → "lodash"
31
- * "@babel/core@npm:^7.0.0" → "@babel/core"
32
- * "@babel/core@npm:^7.0.0, @babel/core@npm:^7.12.3" → "@babel/core"
33
- * "@ngageoint/simple-features-js@patch:@ngageoint/simple-features-js@npm:1.1.0#..." → "@ngageoint/simple-features-js"
108
+ * Extract package name from yarn berry key (fallback for when resolution is unavailable).
109
+ *
110
+ * WARNING: Keys can contain npm aliases. Prefer parseResolution() when possible.
111
+ * The key may return an alias name instead of the real package name.
34
112
  *
35
113
  * @param {string} key - Lockfile entry key
36
- * @returns {string} Package name
114
+ * @returns {string} Package name (may be alias name, not canonical name)
115
+ *
116
+ * @example
117
+ * // Simple unscoped package
118
+ * parseLockfileKey('lodash@npm:^4.17.21')
119
+ * // => 'lodash'
120
+ *
121
+ * @example
122
+ * // Scoped package
123
+ * parseLockfileKey('@babel/core@npm:^7.24.0')
124
+ * // => '@babel/core'
125
+ *
126
+ * @example
127
+ * // Multiple version ranges (comma-separated) - takes first entry
128
+ * parseLockfileKey('@types/node@npm:^18.0.0, @types/node@npm:^20.0.0')
129
+ * // => '@types/node'
130
+ *
131
+ * @example
132
+ * // npm alias - returns the ALIAS name (not real package)
133
+ * // Use parseResolution() for the real package name
134
+ * parseLockfileKey('string-width-cjs@npm:string-width@^4.2.0')
135
+ * // => 'string-width-cjs'
136
+ *
137
+ * @example
138
+ * // Scoped npm alias
139
+ * parseLockfileKey('@babel-baseline/core@npm:@babel/core@7.24.4')
140
+ * // => '@babel-baseline/core'
141
+ *
142
+ * @example
143
+ * // Workspace protocol
144
+ * parseLockfileKey('my-pkg@workspace:packages/my-pkg')
145
+ * // => 'my-pkg'
146
+ *
147
+ * @example
148
+ * // Scoped workspace package
149
+ * parseLockfileKey('@myorg/utils@workspace:.')
150
+ * // => '@myorg/utils'
151
+ *
152
+ * @example
153
+ * // Portal protocol
154
+ * parseLockfileKey('external-pkg@portal:../some/path')
155
+ * // => 'external-pkg'
156
+ *
157
+ * @example
158
+ * // Link protocol
159
+ * parseLockfileKey('linked-pkg@link:./local')
160
+ * // => 'linked-pkg'
161
+ *
162
+ * @example
163
+ * // Patch protocol (complex nested format)
164
+ * parseLockfileKey('pkg@patch:pkg@npm:1.0.0#./patches/fix.patch')
165
+ * // => 'pkg'
166
+ *
167
+ * @example
168
+ * // Scoped patch
169
+ * parseLockfileKey('@scope/pkg@patch:@scope/pkg@npm:1.0.0#./fix.patch')
170
+ * // => '@scope/pkg'
171
+ *
172
+ * @example
173
+ * // File protocol
174
+ * parseLockfileKey('local-pkg@file:../local-package')
175
+ * // => 'local-pkg'
37
176
  */
38
- function extractName(key) {
177
+ export function parseLockfileKey(key) {
39
178
  // Keys can have multiple comma-separated entries, take the first one
40
179
  const firstKey = key.split(',')[0].trim();
41
180
 
@@ -73,29 +212,39 @@ function extractName(key) {
73
212
 
74
213
  /**
75
214
  * Parse yarn.lock v2+ (berry)
76
- * @param {string} content - Lockfile content
77
- * @param {Object} [options] - Parser options
215
+ * @param {string | object} input - Lockfile content string or pre-parsed object
216
+ * @param {Object} [_options] - Parser options (unused, reserved for future use)
78
217
  * @returns {Generator<Dependency>}
79
218
  */
80
- export function* fromYarnBerryLock(content, _options = {}) {
81
- const lockfile = parseSyml(content);
219
+ export function* fromYarnBerryLock(input, _options = {}) {
220
+ const lockfile = typeof input === 'string' ? parseSyml(input) : input;
82
221
 
83
222
  for (const [key, pkg] of Object.entries(lockfile)) {
84
223
  // Skip metadata
85
224
  if (key === '__metadata') continue;
86
225
 
87
- const name = extractName(key);
88
226
  const { version, checksum, resolution } = pkg;
89
227
 
90
- // Check if this is a link (workspace:, portal:, or link: protocol)
91
- const link = resolution?.startsWith('workspace:')
92
- || resolution?.startsWith('portal:')
93
- || resolution?.startsWith('link:');
228
+ // Check if this is a local/workspace entry (workspace:, portal:, or link: protocol)
229
+ // The protocol appears after @ in both key and resolution: "pkg@workspace:..."
230
+ const link =
231
+ key.includes('@workspace:') ||
232
+ key.includes('@portal:') ||
233
+ key.includes('@link:') ||
234
+ resolution?.includes('@workspace:') ||
235
+ resolution?.includes('@portal:') ||
236
+ resolution?.includes('@link:');
94
237
 
95
238
  // Skip workspace/link entries - flatlock only cares about external dependencies
96
239
  if (link) continue;
97
240
 
241
+ // Use the resolution field for the package name - it's the canonical identifier
242
+ // Keys can contain npm aliases (e.g., "string-width-cjs@npm:string-width@^4.2.0")
243
+ // but resolution always has the actual package name (e.g., "string-width@npm:4.2.3")
244
+ const name = parseResolution(resolution) || parseLockfileKey(key);
245
+
98
246
  if (name && version) {
247
+ /** @type {Dependency} */
99
248
  const dep = { name, version };
100
249
  if (checksum) dep.integrity = checksum;
101
250
  if (resolution) dep.resolved = resolution;
@@ -1,16 +1,19 @@
1
1
  import yarnLockfile from '@yarnpkg/lockfile';
2
2
 
3
- const { parse } = yarnLockfile;
3
+ /** @typedef {import('./types.js').Dependency} Dependency */
4
4
 
5
5
  /**
6
- * @typedef {Object} Dependency
7
- * @property {string} name - Package name
8
- * @property {string} version - Resolved version
9
- * @property {string} [integrity] - Integrity hash
10
- * @property {string} [resolved] - Resolution URL
11
- * @property {boolean} [link] - True if this is a symlink
6
+ * @typedef {Object} YarnClassicParseResult
7
+ * @property {'success' | 'merge' | 'conflict'} type - Parse result type
8
+ * @property {Record<string, any>} object - Parsed lockfile object
12
9
  */
13
10
 
11
+ /**
12
+ * The yarn classic parse function (handles CJS/ESM interop)
13
+ * @type {(content: string) => YarnClassicParseResult}
14
+ */
15
+ export const parseYarnClassic = yarnLockfile.default?.parse || yarnLockfile.parse;
16
+
14
17
  /**
15
18
  * !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
16
19
  * !! WARNING: DO NOT MODIFY THIS FUNCTION !!
@@ -27,16 +30,70 @@ const { parse } = yarnLockfile;
27
30
  *
28
31
  * Extract package name from yarn classic key.
29
32
  *
30
- * Examples:
31
- * "lodash@^4.17.21" → "lodash"
32
- * "@babel/core@^7.0.0" → "@babel/core"
33
- * "lodash@^4.17.21, lodash@^4.0.0" → "lodash" (multiple version ranges)
34
- * "@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3" → "@babel/traverse--for-generate-function-map"
35
- *
36
33
  * @param {string} key - Lockfile entry key
37
34
  * @returns {string} Package name
35
+ *
36
+ * @example
37
+ * // Simple unscoped package with semver range
38
+ * parseLockfileKey('lodash@^4.17.21')
39
+ * // => 'lodash'
40
+ *
41
+ * @example
42
+ * // Scoped package
43
+ * parseLockfileKey('@babel/core@^7.0.0')
44
+ * // => '@babel/core'
45
+ *
46
+ * @example
47
+ * // Multiple version ranges (comma-separated) - takes first entry
48
+ * parseLockfileKey('lodash@^4.17.21, lodash@^4.0.0')
49
+ * // => 'lodash'
50
+ *
51
+ * @example
52
+ * // Multiple ranges for scoped package
53
+ * parseLockfileKey('@types/node@^18.0.0, @types/node@^20.0.0')
54
+ * // => '@types/node'
55
+ *
56
+ * @example
57
+ * // npm: alias protocol - returns the ALIAS name
58
+ * parseLockfileKey('@babel/traverse--for-generate-function-map@npm:@babel/traverse@^7.25.3')
59
+ * // => '@babel/traverse--for-generate-function-map'
60
+ *
61
+ * @example
62
+ * // Unscoped alias
63
+ * parseLockfileKey('string-width-cjs@npm:string-width@^4.2.0')
64
+ * // => 'string-width-cjs'
65
+ *
66
+ * @example
67
+ * // Exact version
68
+ * parseLockfileKey('typescript@5.3.3')
69
+ * // => 'typescript'
70
+ *
71
+ * @example
72
+ * // Git URL specifier
73
+ * parseLockfileKey('my-lib@github:user/repo#v1.0.0')
74
+ * // => 'my-lib'
75
+ *
76
+ * @example
77
+ * // Tarball URL
78
+ * parseLockfileKey('custom-pkg@https://example.com/pkg.tgz')
79
+ * // => 'custom-pkg'
80
+ *
81
+ * @example
82
+ * // Package with prerelease version
83
+ * parseLockfileKey('@next/env@^14.0.0-canary.0')
84
+ * // => '@next/env'
85
+ *
86
+ * @example
87
+ * // Package without @ (bare name, edge case)
88
+ * parseLockfileKey('lodash')
89
+ * // => 'lodash'
90
+ *
91
+ * @example
92
+ * // Deeply scoped alias pointing to scoped package
93
+ * parseLockfileKey('@myorg/my-alias@npm:@original/package@^1.0.0')
94
+ * // => '@myorg/my-alias'
38
95
  */
39
- function extractName(key) {
96
+ export function parseLockfileKey(key) {
40
97
  // Keys can have multiple version ranges: "pkg@^1.0.0, pkg@^2.0.0"
41
98
  // Take the first part before comma
42
99
  const firstKey = key.split(',')[0].trim();
@@ -72,21 +129,24 @@ function extractName(key) {
72
129
 
73
130
  /**
74
131
  * Parse yarn.lock v1 (classic)
75
- * @param {string} content - Lockfile content
76
- * @param {Object} [options] - Parser options
132
+ * @param {string | object} input - Lockfile content string or pre-parsed object
133
+ * @param {Object} [_options] - Parser options (unused, reserved for future use)
77
134
  * @returns {Generator<Dependency>}
78
135
  */
79
- export function* fromYarnClassicLock(content, _options = {}) {
80
- const parsed = parse(content);
81
-
82
- if (parsed.type !== 'success') {
83
- throw new Error(`Failed to parse yarn.lock: ${parsed.type}`);
136
+ export function* fromYarnClassicLock(input, _options = {}) {
137
+ let lockfile;
138
+ if (typeof input === 'string') {
139
+ const result = parseYarnClassic(input);
140
+ if (result.type !== 'success' && result.type !== 'merge') {
141
+ throw new Error('Failed to parse yarn.lock');
142
+ }
143
+ lockfile = result.object;
144
+ } else {
145
+ lockfile = input;
84
146
  }
85
147
 
86
- const lockfile = parsed.object;
87
-
88
148
  for (const [key, pkg] of Object.entries(lockfile)) {
89
- const name = extractName(key);
149
+ const name = parseLockfileKey(key);
90
150
  const { version, integrity, resolved } = pkg;
91
151
 
92
152
  // Check if this is a link (file: or link: protocol)
@@ -96,6 +156,7 @@ export function* fromYarnClassicLock(content, _options = {}) {
96
156
  if (link) continue;
97
157
 
98
158
  if (name && version) {
159
+ /** @type {Dependency} */
99
160
  const dep = { name, version };
100
161
  if (integrity) dep.integrity = integrity;
101
162
  if (resolved) dep.resolved = resolved;
package/src/result.js CHANGED
@@ -22,14 +22,14 @@
22
22
  * @param {T} value - The success value
23
23
  * @returns {OkResult<T>}
24
24
  */
25
- export const Ok = (value) => ({ ok: true, value });
25
+ export const Ok = value => ({ ok: true, value });
26
26
 
27
27
  /**
28
28
  * Create a failed Result
29
29
  * @param {Error | string} error - The error
30
30
  * @returns {ErrResult}
31
31
  */
32
- export const Err = (error) => ({
32
+ export const Err = error => ({
33
33
  ok: false,
34
34
  error: error instanceof Error ? error : new Error(error)
35
35
  });