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
package/src/index.js CHANGED
@@ -1,17 +1,21 @@
1
1
  import { readFile } from 'node:fs/promises';
2
- import { Type, detectType } from './detect.js';
3
- import { Ok, Err } from './result.js';
2
+ import { detectType, Type } from './detect.js';
4
3
  import {
5
4
  fromPackageLock,
6
5
  fromPnpmLock,
7
- fromYarnClassicLock,
8
- fromYarnBerryLock
6
+ fromYarnBerryLock,
7
+ fromYarnClassicLock
9
8
  } from './parsers/index.js';
9
+ import { Err, Ok } from './result.js';
10
+ import { FlatlockSet } from './set.js';
11
+
12
+ /** @typedef {import('./detect.js').LockfileType} LockfileType */
13
+ /** @typedef {import('./parsers/npm.js').Dependency} Dependency */
10
14
 
11
15
  /**
12
- * @typedef {import('./detect.js').LockfileType} LockfileType
13
- * @typedef {import('./result.js').Result} Result
14
- * @typedef {import('./parsers/npm.js').Dependency} Dependency
16
+ * @typedef {Object} ParseOptions
17
+ * @property {string} [path] - Path hint for type detection
18
+ * @property {LockfileType} [type] - Explicit type (skip detection)
15
19
  */
16
20
 
17
21
  // Re-export Type and detection
@@ -23,6 +27,9 @@ export { Ok, Err };
23
27
  // Re-export individual parsers
24
28
  export { fromPackageLock, fromPnpmLock, fromYarnClassicLock, fromYarnBerryLock };
25
29
 
30
+ // Re-export FlatlockSet class
31
+ export { FlatlockSet };
32
+
26
33
  /**
27
34
  * Parse lockfile from path (auto-detect type)
28
35
  * @param {string} path - Path to lockfile
@@ -73,23 +80,23 @@ export function* fromString(content, options = {}) {
73
80
  /**
74
81
  * Try to parse lockfile from path (returns Result)
75
82
  * @param {string} path - Path to lockfile
76
- * @param {Object} [options] - Parser options
77
- * @returns {Promise<Result<AsyncGenerator<Dependency>>>}
83
+ * @param {ParseOptions} [options] - Parser options
84
+ * @returns {Promise<import('./result.js').Result<AsyncGenerator<Dependency>>>}
78
85
  */
79
86
  export async function tryFromPath(path, options = {}) {
80
87
  try {
81
88
  const generator = fromPath(path, options);
82
89
  return Ok(generator);
83
90
  } catch (err) {
84
- return Err(err);
91
+ return Err(/** @type {Error} */ (err));
85
92
  }
86
93
  }
87
94
 
88
95
  /**
89
96
  * Try to parse lockfile from string (returns Result)
90
97
  * @param {string} content - Lockfile content
91
- * @param {Object} [options] - Parser options
92
- * @returns {Result<Generator<Dependency>>}
98
+ * @param {ParseOptions} [options] - Parser options
99
+ * @returns {import('./result.js').Result<Generator<Dependency>>}
93
100
  */
94
101
  export function tryFromString(content, options = {}) {
95
102
  try {
@@ -98,7 +105,7 @@ export function tryFromString(content, options = {}) {
98
105
  const generator = fromString(content, { ...options, type });
99
106
  return Ok(generator);
100
107
  } catch (err) {
101
- return Err(err);
108
+ return Err(/** @type {Error} */ (err));
102
109
  }
103
110
  }
104
111
 
@@ -127,8 +134,8 @@ export function* fromYarnLock(content, options = {}) {
127
134
  export async function collect(pathOrContent, options = {}) {
128
135
  const deps = [];
129
136
 
130
- // Check if it's a path or content
131
- const isPath = !pathOrContent.includes('\n') && !pathOrContent.startsWith('{');
137
+ // Better heuristic: paths don't contain newlines and are reasonably short
138
+ const isPath = !pathOrContent.includes('\n') && pathOrContent.length < 1000;
132
139
 
133
140
  if (isPath) {
134
141
  for await (const dep of fromPath(pathOrContent, options)) {
@@ -142,3 +149,14 @@ export async function collect(pathOrContent, options = {}) {
142
149
 
143
150
  return deps;
144
151
  }
152
+
153
+ // Re-export compare API
154
+ export { compare, compareAll, getAvailableParsers } from './compare.js';
155
+
156
+ // Re-export lockfile key parsing utilities
157
+ export {
158
+ parseNpmKey,
159
+ parsePnpmKey,
160
+ parseYarnBerryKey,
161
+ parseYarnClassicKey
162
+ } from './parsers/index.js';
@@ -2,7 +2,15 @@
2
2
  * Re-export all lockfile parsers
3
3
  */
4
4
 
5
- export { fromPackageLock } from './npm.js';
6
- export { fromPnpmLock } from './pnpm.js';
7
- export { fromYarnClassicLock } from './yarn-classic.js';
8
- export { fromYarnBerryLock } from './yarn-berry.js';
5
+ export { fromPackageLock, parseLockfileKey as parseNpmKey } from './npm.js';
6
+ export { fromPnpmLock, parseLockfileKey as parsePnpmKey } from './pnpm.js';
7
+ export {
8
+ fromYarnBerryLock,
9
+ parseLockfileKey as parseYarnBerryKey,
10
+ parseResolution as parseYarnBerryResolution
11
+ } from './yarn-berry.js';
12
+ export {
13
+ fromYarnClassicLock,
14
+ parseLockfileKey as parseYarnClassicKey,
15
+ parseYarnClassic
16
+ } from './yarn-classic.js';
@@ -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,32 +36,85 @@
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
- function extractPackageName(path) {
102
+ export function parseLockfileKey(path) {
55
103
  const parts = path.split('/');
56
- const name = parts.at(-1);
104
+ const name = /** @type {string} */ (parts.at(-1));
57
105
  const maybeScope = parts.at(-2);
58
106
 
59
- return maybeScope?.startsWith('@')
60
- ? `${maybeScope}/${name}`
61
- : name;
107
+ return maybeScope?.startsWith('@') ? `${maybeScope}/${name}` : name;
62
108
  }
63
109
 
64
110
  /**
65
111
  * Parse npm package-lock.json (v1, v2, v3)
66
- * @param {string} content - Lockfile content
67
- * @param {Object} [options] - Parser options
112
+ * @param {string | object} input - Lockfile content string or pre-parsed object
113
+ * @param {Object} [_options] - Parser options (unused, reserved for future use)
68
114
  * @returns {Generator<Dependency>}
69
115
  */
70
- export function* fromPackageLock(content, _options = {}) {
71
- const lockfile = JSON.parse(content);
116
+ export function* fromPackageLock(input, _options = {}) {
117
+ const lockfile = typeof input === 'string' ? JSON.parse(input) : input;
72
118
  const packages = lockfile.packages || {};
73
119
 
74
120
  for (const [path, pkg] of Object.entries(packages)) {
@@ -81,11 +127,12 @@ export function* fromPackageLock(content, _options = {}) {
81
127
  // 2. node_modules/<workspace-package.json-name> → link, NO version (symlink)
82
128
  if (!path.includes('node_modules/')) continue;
83
129
 
84
- const name = extractPackageName(path);
130
+ const name = parseLockfileKey(path);
85
131
  const { version, integrity, resolved, link } = pkg;
86
132
 
87
133
  // Only yield if we have a name and version
88
134
  if (name && version) {
135
+ /** @type {Dependency} */
89
136
  const dep = { name, version };
90
137
  if (integrity) dep.integrity = integrity;
91
138
  if (resolved) dep.resolved = resolved;
@@ -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
+ }