flatlock 1.1.0 → 1.3.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 +95 -1
- package/bin/flatcover.js +398 -0
- package/bin/flatlock-cmp.js +71 -45
- package/bin/flatlock.js +158 -0
- package/package.json +21 -8
- package/src/compare.js +385 -28
- package/src/detect.js +3 -4
- package/src/index.js +9 -2
- package/src/parsers/index.js +24 -4
- package/src/parsers/npm.js +144 -14
- package/src/parsers/pnpm/detect.js +198 -0
- package/src/parsers/pnpm/index.js +359 -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 +271 -36
- package/src/parsers/yarn-classic.js +81 -21
- package/src/set.js +1307 -0
- package/dist/compare.d.ts +0 -63
- package/dist/compare.d.ts.map +0 -1
- package/dist/detect.d.ts +0 -33
- package/dist/detect.d.ts.map +0 -1
- package/dist/index.d.ts +0 -70
- package/dist/index.d.ts.map +0 -1
- package/dist/parsers/index.d.ts +0 -5
- package/dist/parsers/index.d.ts.map +0 -1
- package/dist/parsers/npm.d.ts +0 -82
- package/dist/parsers/npm.d.ts.map +0 -1
- package/dist/parsers/pnpm.d.ts +0 -60
- package/dist/parsers/pnpm.d.ts.map +0 -1
- package/dist/parsers/yarn-berry.d.ts +0 -65
- package/dist/parsers/yarn-berry.d.ts.map +0 -1
- package/dist/parsers/yarn-classic.d.ts +0 -64
- package/dist/parsers/yarn-classic.d.ts.map +0 -1
- package/dist/result.d.ts +0 -12
- package/dist/result.d.ts.map +0 -1
package/src/parsers/npm.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** @typedef {import('./types.js').Dependency} Dependency */
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
|
-
* @typedef {Object}
|
|
3
|
-
* @property {string} name
|
|
4
|
-
* @property {string} version
|
|
5
|
-
* @property {string} [
|
|
6
|
-
* @property {string} [
|
|
7
|
-
* @property {
|
|
7
|
+
* @typedef {Object} WorkspacePackage
|
|
8
|
+
* @property {string} name
|
|
9
|
+
* @property {string} version
|
|
10
|
+
* @property {Record<string, string>} [dependencies]
|
|
11
|
+
* @property {Record<string, string>} [devDependencies]
|
|
12
|
+
* @property {Record<string, string>} [optionalDependencies]
|
|
13
|
+
* @property {Record<string, string>} [peerDependencies]
|
|
8
14
|
*/
|
|
9
15
|
|
|
10
16
|
/**
|
|
@@ -43,13 +49,68 @@
|
|
|
43
49
|
* pkg := name (unscoped)
|
|
44
50
|
* | @scope/name (scoped)
|
|
45
51
|
*
|
|
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
52
|
* @param {string} path - Lockfile path key
|
|
52
53
|
* @returns {string} Package name
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // Simple unscoped package
|
|
57
|
+
* parseLockfileKey('node_modules/lodash')
|
|
58
|
+
* // => 'lodash'
|
|
59
|
+
*
|
|
60
|
+
* @example
|
|
61
|
+
* // Scoped package
|
|
62
|
+
* parseLockfileKey('node_modules/@babel/core')
|
|
63
|
+
* // => '@babel/core'
|
|
64
|
+
*
|
|
65
|
+
* @example
|
|
66
|
+
* // Nested dependency (hoisted conflict resolution)
|
|
67
|
+
* parseLockfileKey('node_modules/foo/node_modules/bar')
|
|
68
|
+
* // => 'bar'
|
|
69
|
+
*
|
|
70
|
+
* @example
|
|
71
|
+
* // Nested scoped dependency
|
|
72
|
+
* parseLockfileKey('node_modules/foo/node_modules/@scope/bar')
|
|
73
|
+
* // => '@scope/bar'
|
|
74
|
+
*
|
|
75
|
+
* @example
|
|
76
|
+
* // Deeply nested dependency
|
|
77
|
+
* parseLockfileKey('node_modules/a/node_modules/b/node_modules/c')
|
|
78
|
+
* // => 'c'
|
|
79
|
+
*
|
|
80
|
+
* @example
|
|
81
|
+
* // Deeply nested scoped dependency
|
|
82
|
+
* parseLockfileKey('node_modules/a/node_modules/@types/node')
|
|
83
|
+
* // => '@types/node'
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* // Workspace package path (definition)
|
|
87
|
+
* parseLockfileKey('packages/my-lib')
|
|
88
|
+
* // => 'my-lib'
|
|
89
|
+
*
|
|
90
|
+
* @example
|
|
91
|
+
* // Workspace nested dependency
|
|
92
|
+
* parseLockfileKey('packages/my-lib/node_modules/lodash')
|
|
93
|
+
* // => 'lodash'
|
|
94
|
+
*
|
|
95
|
+
* @example
|
|
96
|
+
* // Workspace nested scoped dependency
|
|
97
|
+
* parseLockfileKey('packages/my-lib/node_modules/@types/react')
|
|
98
|
+
* // => '@types/react'
|
|
99
|
+
*
|
|
100
|
+
* @example
|
|
101
|
+
* // Package with hyphenated name
|
|
102
|
+
* parseLockfileKey('node_modules/string-width')
|
|
103
|
+
* // => 'string-width'
|
|
104
|
+
*
|
|
105
|
+
* @example
|
|
106
|
+
* // Scoped package with hyphenated name
|
|
107
|
+
* parseLockfileKey('node_modules/@emotion/styled')
|
|
108
|
+
* // => '@emotion/styled'
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* // Complex nested path
|
|
112
|
+
* parseLockfileKey('node_modules/@babel/core/node_modules/@babel/helper-compilation-targets')
|
|
113
|
+
* // => '@babel/helper-compilation-targets'
|
|
53
114
|
*/
|
|
54
115
|
export function parseLockfileKey(path) {
|
|
55
116
|
const parts = path.split('/');
|
|
@@ -61,12 +122,12 @@ export function parseLockfileKey(path) {
|
|
|
61
122
|
|
|
62
123
|
/**
|
|
63
124
|
* Parse npm package-lock.json (v1, v2, v3)
|
|
64
|
-
* @param {string}
|
|
125
|
+
* @param {string | object} input - Lockfile content string or pre-parsed object
|
|
65
126
|
* @param {Object} [_options] - Parser options (unused, reserved for future use)
|
|
66
127
|
* @returns {Generator<Dependency>}
|
|
67
128
|
*/
|
|
68
|
-
export function* fromPackageLock(
|
|
69
|
-
const lockfile = JSON.parse(
|
|
129
|
+
export function* fromPackageLock(input, _options = {}) {
|
|
130
|
+
const lockfile = typeof input === 'string' ? JSON.parse(input) : input;
|
|
70
131
|
const packages = lockfile.packages || {};
|
|
71
132
|
|
|
72
133
|
for (const [path, pkg] of Object.entries(packages)) {
|
|
@@ -93,3 +154,72 @@ export function* fromPackageLock(content, _options = {}) {
|
|
|
93
154
|
}
|
|
94
155
|
}
|
|
95
156
|
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Extract workspace paths from npm lockfile.
|
|
160
|
+
*
|
|
161
|
+
* npm workspace packages are entries in `packages` that:
|
|
162
|
+
* - Are not the root ('')
|
|
163
|
+
* - Don't contain 'node_modules/' (those are installed deps)
|
|
164
|
+
* - Have a version field
|
|
165
|
+
*
|
|
166
|
+
* @param {string | object} input - Lockfile content string or pre-parsed object
|
|
167
|
+
* @returns {string[]} Array of workspace paths (e.g., ['workspaces/arborist', 'workspaces/libnpmfund'])
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* extractWorkspacePaths(lockfile)
|
|
171
|
+
* // => ['workspaces/arborist', 'workspaces/libnpmfund', ...]
|
|
172
|
+
*/
|
|
173
|
+
export function extractWorkspacePaths(input) {
|
|
174
|
+
const lockfile = typeof input === 'string' ? JSON.parse(input) : input;
|
|
175
|
+
const packages = lockfile.packages || {};
|
|
176
|
+
const paths = [];
|
|
177
|
+
|
|
178
|
+
for (const [path, pkg] of Object.entries(packages)) {
|
|
179
|
+
// Skip root and node_modules entries
|
|
180
|
+
if (path === '' || path.includes('node_modules')) continue;
|
|
181
|
+
|
|
182
|
+
// Workspace entries have a version
|
|
183
|
+
if (pkg.version) {
|
|
184
|
+
paths.push(path);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return paths;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Build workspace packages map by reading package.json files.
|
|
193
|
+
*
|
|
194
|
+
* @param {string | object} input - Lockfile content string or pre-parsed object
|
|
195
|
+
* @param {string} repoDir - Path to repository root
|
|
196
|
+
* @returns {Promise<Record<string, WorkspacePackage>>} Map of workspace path to package info
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* const workspaces = await buildWorkspacePackages(lockfile, '/path/to/repo');
|
|
200
|
+
* // => { 'workspaces/arborist': { name: '@npmcli/arborist', version: '1.0.0', dependencies: {...} } }
|
|
201
|
+
*/
|
|
202
|
+
export async function buildWorkspacePackages(input, repoDir) {
|
|
203
|
+
const paths = extractWorkspacePaths(input);
|
|
204
|
+
/** @type {Record<string, WorkspacePackage>} */
|
|
205
|
+
const workspacePackages = {};
|
|
206
|
+
|
|
207
|
+
for (const wsPath of paths) {
|
|
208
|
+
const pkgJsonPath = join(repoDir, wsPath, 'package.json');
|
|
209
|
+
try {
|
|
210
|
+
const pkg = JSON.parse(await readFile(pkgJsonPath, 'utf8'));
|
|
211
|
+
workspacePackages[wsPath] = {
|
|
212
|
+
name: pkg.name,
|
|
213
|
+
version: pkg.version || '0.0.0',
|
|
214
|
+
dependencies: pkg.dependencies,
|
|
215
|
+
devDependencies: pkg.devDependencies,
|
|
216
|
+
optionalDependencies: pkg.optionalDependencies,
|
|
217
|
+
peerDependencies: pkg.peerDependencies
|
|
218
|
+
};
|
|
219
|
+
} catch {
|
|
220
|
+
// Skip workspaces with missing or invalid package.json
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return workspacePackages;
|
|
225
|
+
}
|
|
@@ -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
|
+
}
|