flatlock 1.2.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.
Files changed (43) hide show
  1. package/README.md +42 -1
  2. package/bin/flatcover.js +398 -0
  3. package/bin/flatlock.js +158 -0
  4. package/package.json +16 -5
  5. package/src/parsers/index.js +14 -2
  6. package/src/parsers/npm.js +82 -0
  7. package/src/parsers/pnpm/index.js +70 -0
  8. package/src/parsers/yarn-berry.js +88 -0
  9. package/src/set.js +730 -41
  10. package/dist/compare.d.ts +0 -85
  11. package/dist/compare.d.ts.map +0 -1
  12. package/dist/detect.d.ts +0 -33
  13. package/dist/detect.d.ts.map +0 -1
  14. package/dist/index.d.ts +0 -72
  15. package/dist/index.d.ts.map +0 -1
  16. package/dist/parsers/index.d.ts +0 -5
  17. package/dist/parsers/index.d.ts.map +0 -1
  18. package/dist/parsers/npm.d.ts +0 -109
  19. package/dist/parsers/npm.d.ts.map +0 -1
  20. package/dist/parsers/pnpm/detect.d.ts +0 -136
  21. package/dist/parsers/pnpm/detect.d.ts.map +0 -1
  22. package/dist/parsers/pnpm/index.d.ts +0 -120
  23. package/dist/parsers/pnpm/index.d.ts.map +0 -1
  24. package/dist/parsers/pnpm/internal.d.ts +0 -5
  25. package/dist/parsers/pnpm/internal.d.ts.map +0 -1
  26. package/dist/parsers/pnpm/shrinkwrap.d.ts +0 -129
  27. package/dist/parsers/pnpm/shrinkwrap.d.ts.map +0 -1
  28. package/dist/parsers/pnpm/v5.d.ts +0 -139
  29. package/dist/parsers/pnpm/v5.d.ts.map +0 -1
  30. package/dist/parsers/pnpm/v6plus.d.ts +0 -212
  31. package/dist/parsers/pnpm/v6plus.d.ts.map +0 -1
  32. package/dist/parsers/pnpm.d.ts +0 -2
  33. package/dist/parsers/pnpm.d.ts.map +0 -1
  34. package/dist/parsers/types.d.ts +0 -23
  35. package/dist/parsers/types.d.ts.map +0 -1
  36. package/dist/parsers/yarn-berry.d.ts +0 -154
  37. package/dist/parsers/yarn-berry.d.ts.map +0 -1
  38. package/dist/parsers/yarn-classic.d.ts +0 -110
  39. package/dist/parsers/yarn-classic.d.ts.map +0 -1
  40. package/dist/result.d.ts +0 -12
  41. package/dist/result.d.ts.map +0 -1
  42. package/dist/set.d.ts +0 -189
  43. package/dist/set.d.ts.map +0 -1
@@ -1,5 +1,18 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+
1
4
  /** @typedef {import('./types.js').Dependency} Dependency */
2
5
 
6
+ /**
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]
14
+ */
15
+
3
16
  /**
4
17
  * LIMITATION: Workspace symlinks are not yielded
5
18
  *
@@ -141,3 +154,72 @@ export function* fromPackageLock(input, _options = {}) {
141
154
  }
142
155
  }
143
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
+ }
@@ -11,6 +11,8 @@
11
11
  * @module flatlock/parsers/pnpm
12
12
  */
13
13
 
14
+ import { readFile } from 'node:fs/promises';
15
+ import { join } from 'node:path';
14
16
  import yaml from 'js-yaml';
15
17
 
16
18
  import { detectVersion } from './detect.js';
@@ -20,6 +22,16 @@ import { parseSpecV6Plus } from './v6plus.js';
20
22
 
21
23
  /** @typedef {import('../types.js').Dependency} Dependency */
22
24
 
25
+ /**
26
+ * @typedef {Object} WorkspacePackage
27
+ * @property {string} name
28
+ * @property {string} version
29
+ * @property {Record<string, string>} [dependencies]
30
+ * @property {Record<string, string>} [devDependencies]
31
+ * @property {Record<string, string>} [optionalDependencies]
32
+ * @property {Record<string, string>} [peerDependencies]
33
+ */
34
+
23
35
  // Public API: detectVersion for users who need to inspect lockfile version
24
36
  export { detectVersion } from './detect.js';
25
37
 
@@ -287,3 +299,61 @@ export function* fromPnpmLock(input, _options = {}) {
287
299
  // Note: importers (workspace packages) are intentionally NOT yielded
288
300
  // flatlock only cares about external dependencies
289
301
  }
302
+
303
+ /**
304
+ * Extract workspace paths from pnpm lockfile.
305
+ *
306
+ * pnpm stores workspace packages in the `importers` section.
307
+ * Each key is a workspace path relative to the repo root.
308
+ *
309
+ * @param {string | object} input - Lockfile content string or pre-parsed object
310
+ * @returns {string[]} Array of workspace paths (e.g., ['packages/foo', 'packages/bar'])
311
+ *
312
+ * @example
313
+ * extractWorkspacePaths(lockfile)
314
+ * // => ['packages/vue', 'packages/compiler-core', ...]
315
+ */
316
+ export function extractWorkspacePaths(input) {
317
+ const lockfile = /** @type {Record<string, any>} */ (
318
+ typeof input === 'string' ? yaml.load(input) : input
319
+ );
320
+
321
+ const importers = lockfile.importers || {};
322
+ return Object.keys(importers).filter(k => k !== '.');
323
+ }
324
+
325
+ /**
326
+ * Build workspace packages map by reading package.json files.
327
+ *
328
+ * @param {string | object} input - Lockfile content string or pre-parsed object
329
+ * @param {string} repoDir - Path to repository root
330
+ * @returns {Promise<Record<string, WorkspacePackage>>} Map of workspace path to package info
331
+ *
332
+ * @example
333
+ * const workspaces = await buildWorkspacePackages(lockfile, '/path/to/repo');
334
+ * // => { 'packages/foo': { name: '@scope/foo', version: '1.0.0', dependencies: {...} } }
335
+ */
336
+ export async function buildWorkspacePackages(input, repoDir) {
337
+ const paths = extractWorkspacePaths(input);
338
+ /** @type {Record<string, WorkspacePackage>} */
339
+ const workspacePackages = {};
340
+
341
+ for (const wsPath of paths) {
342
+ const pkgJsonPath = join(repoDir, wsPath, 'package.json');
343
+ try {
344
+ const pkg = JSON.parse(await readFile(pkgJsonPath, 'utf8'));
345
+ workspacePackages[wsPath] = {
346
+ name: pkg.name,
347
+ version: pkg.version || '0.0.0',
348
+ dependencies: pkg.dependencies,
349
+ devDependencies: pkg.devDependencies,
350
+ optionalDependencies: pkg.optionalDependencies,
351
+ peerDependencies: pkg.peerDependencies
352
+ };
353
+ } catch {
354
+ // Skip workspaces with missing or invalid package.json
355
+ }
356
+ }
357
+
358
+ return workspacePackages;
359
+ }
@@ -1,7 +1,19 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
1
3
  import { parseSyml } from '@yarnpkg/parsers';
2
4
 
3
5
  /** @typedef {import('./types.js').Dependency} Dependency */
4
6
 
7
+ /**
8
+ * @typedef {Object} WorkspacePackage
9
+ * @property {string} name
10
+ * @property {string} version
11
+ * @property {Record<string, string>} [dependencies]
12
+ * @property {Record<string, string>} [devDependencies]
13
+ * @property {Record<string, string>} [optionalDependencies]
14
+ * @property {Record<string, string>} [peerDependencies]
15
+ */
16
+
5
17
  /**
6
18
  * Extract package name from yarn berry resolution field.
7
19
  *
@@ -252,3 +264,79 @@ export function* fromYarnBerryLock(input, _options = {}) {
252
264
  }
253
265
  }
254
266
  }
267
+
268
+ /**
269
+ * Extract workspace paths from yarn berry lockfile.
270
+ *
271
+ * Yarn berry workspace entries use `@workspace:` protocol in keys.
272
+ * Keys can have multiple comma-separated descriptors.
273
+ *
274
+ * @param {string | object} input - Lockfile content string or pre-parsed object
275
+ * @returns {string[]} Array of workspace paths (e.g., ['packages/foo', 'packages/bar'])
276
+ *
277
+ * @example
278
+ * extractWorkspacePaths(lockfile)
279
+ * // => ['packages/babel-core', 'packages/babel-parser', ...]
280
+ */
281
+ export function extractWorkspacePaths(input) {
282
+ const lockfile = typeof input === 'string' ? parseSyml(input) : input;
283
+ const paths = new Set();
284
+
285
+ for (const key of Object.keys(lockfile)) {
286
+ if (key === '__metadata') continue;
287
+ if (!key.includes('@workspace:')) continue;
288
+
289
+ // Keys can have multiple comma-separated descriptors:
290
+ // "@babel/types@workspace:*, @babel/types@workspace:^, @babel/types@workspace:packages/babel-types"
291
+ const descriptors = key.split(', ');
292
+ for (const desc of descriptors) {
293
+ if (!desc.includes('@workspace:')) continue;
294
+
295
+ const wsIndex = desc.indexOf('@workspace:');
296
+ const path = desc.slice(wsIndex + '@workspace:'.length);
297
+
298
+ // Skip wildcards (*, ^) and root workspace (.)
299
+ if (path && path !== '.' && path !== '*' && path !== '^' && path.includes('/')) {
300
+ paths.add(path);
301
+ }
302
+ }
303
+ }
304
+
305
+ return [...paths];
306
+ }
307
+
308
+ /**
309
+ * Build workspace packages map by reading package.json files.
310
+ *
311
+ * @param {string | object} input - Lockfile content string or pre-parsed object
312
+ * @param {string} repoDir - Path to repository root
313
+ * @returns {Promise<Record<string, WorkspacePackage>>} Map of workspace path to package info
314
+ *
315
+ * @example
316
+ * const workspaces = await buildWorkspacePackages(lockfile, '/path/to/repo');
317
+ * // => { 'packages/foo': { name: '@scope/foo', version: '1.0.0', dependencies: {...} } }
318
+ */
319
+ export async function buildWorkspacePackages(input, repoDir) {
320
+ const paths = extractWorkspacePaths(input);
321
+ /** @type {Record<string, WorkspacePackage>} */
322
+ const workspacePackages = {};
323
+
324
+ for (const wsPath of paths) {
325
+ const pkgJsonPath = join(repoDir, wsPath, 'package.json');
326
+ try {
327
+ const pkg = JSON.parse(await readFile(pkgJsonPath, 'utf8'));
328
+ workspacePackages[wsPath] = {
329
+ name: pkg.name,
330
+ version: pkg.version || '0.0.0',
331
+ dependencies: pkg.dependencies,
332
+ devDependencies: pkg.devDependencies,
333
+ optionalDependencies: pkg.optionalDependencies,
334
+ peerDependencies: pkg.peerDependencies
335
+ };
336
+ } catch {
337
+ // Skip workspaces with missing or invalid package.json
338
+ }
339
+ }
340
+
341
+ return workspacePackages;
342
+ }