Skip to content

Commit 58e5716

Browse files
authored
fix: pnpm workspaces respect exclude flag (#292)
1 parent 6daf50a commit 58e5716

File tree

3 files changed

+166
-4
lines changed

3 files changed

+166
-4
lines changed

lib/dep-graph-builders/pnpm/parse-workspace.ts

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
1+
import { DepGraph } from '@snyk/dep-graph';
12
import * as debugModule from 'debug';
23
import * as path from 'path';
3-
import { parsePkgJson } from '../util';
4+
import { getPnpmLockfileVersion } from '../../utils';
45
import {
56
PackageJsonBase,
67
PnpmProjectParseOptions,
78
ScannedNodeProject,
89
} from '../types';
10+
import { parsePkgJson } from '../util';
911
import { buildDepGraphPnpm } from './build-dep-graph-pnpm';
10-
import { DepGraph } from '@snyk/dep-graph';
12+
import { UNDEFINED_VERSION } from './constants';
1113
import { getPnpmLockfileParser } from './lockfile-parser/index';
1214
import { PnpmLockfileParser } from './lockfile-parser/lockfile-parser';
13-
import { getPnpmLockfileVersion } from '../../utils';
1415
import { getFileContents } from './utils';
15-
import { UNDEFINED_VERSION } from './constants';
1616

1717
const debug = debugModule('snyk-pnpm-workspaces');
1818

@@ -55,6 +55,7 @@ export const parsePnpmWorkspace = async (
5555
includeOptionalDeps,
5656
strictOutOfSync,
5757
pruneWithinTopLevelDeps,
58+
exclude,
5859
} = options;
5960

6061
const pnpmLockfileContents = getFileContents(
@@ -73,7 +74,20 @@ export const parsePnpmWorkspace = async (
7374
Object.keys(lockFileParser.importers),
7475
);
7576

77+
const excludeList = exclude ? exclude.split(',').map((s) => s.trim()) : [];
78+
7679
for (const importer of Object.keys(lockFileParser.importers)) {
80+
if (excludeList.length > 0) {
81+
const importerParts = importer.split('/');
82+
const shouldExclude = excludeList.some((excludeName) =>
83+
importerParts.includes(excludeName),
84+
);
85+
86+
if (shouldExclude) {
87+
debug(`Skipping excluded importer: ${importer}`);
88+
continue;
89+
}
90+
}
7791
const resolvedImporterPath = path.join(workspaceDir, importer);
7892
const packagePath = path.join(resolvedImporterPath, 'package.json');
7993
debug(`Processing project ${packagePath} as part of a pnpm workspace`);

lib/dep-graph-builders/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ export type PnpmProjectParseOptions = {
112112
includeOptionalDeps: boolean;
113113
strictOutOfSync: boolean;
114114
pruneWithinTopLevelDeps: boolean;
115+
exclude?: string;
115116
};
116117

117118
type NodePkgManagers = 'npm' | 'yarn' | 'pnpm';

test/jest/dep-graph-builders/pnpm-workspaces.test.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,3 +313,150 @@ describe('pnpm-lock-v9 catalogs support tests', () => {
313313
);
314314
});
315315
});
316+
317+
describe('pnpm workspaces with exclude option', () => {
318+
describe.each(['pnpm-lock-v5', 'pnpm-lock-v6', 'pnpm-lock-v9'])(
319+
'%s',
320+
(lockFileVersionPath) => {
321+
it('excludes single directory', async () => {
322+
const fixtureName = 'workspace-cyclic';
323+
const result = await parsePnpmWorkspace(
324+
__dirname,
325+
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
326+
{
327+
includeDevDeps: false,
328+
includeOptionalDeps: true,
329+
pruneWithinTopLevelDeps: true,
330+
strictOutOfSync: false,
331+
exclude: 'backend',
332+
},
333+
);
334+
335+
expect(result).toHaveLength(2);
336+
337+
const targetFiles = result.map((r) => r.targetFile.replace(/\\/g, '/'));
338+
expect(targetFiles).toContain(
339+
`fixtures/${lockFileVersionPath}/${fixtureName}/package.json`,
340+
);
341+
expect(targetFiles).toContain(
342+
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/react/package.json`,
343+
);
344+
expect(targetFiles).not.toContain(
345+
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/backend/package.json`,
346+
);
347+
});
348+
349+
it('excludes multiple directories', async () => {
350+
const fixtureName = 'workspace-cyclic';
351+
const result = await parsePnpmWorkspace(
352+
__dirname,
353+
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
354+
{
355+
includeDevDeps: false,
356+
includeOptionalDeps: true,
357+
pruneWithinTopLevelDeps: true,
358+
strictOutOfSync: false,
359+
exclude: 'backend,react',
360+
},
361+
);
362+
363+
expect(result).toHaveLength(1);
364+
365+
const targetFiles = result.map((r) => r.targetFile.replace(/\\/g, '/'));
366+
expect(targetFiles).toContain(
367+
`fixtures/${lockFileVersionPath}/${fixtureName}/package.json`,
368+
);
369+
expect(targetFiles).not.toContain(
370+
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/backend/package.json`,
371+
);
372+
expect(targetFiles).not.toContain(
373+
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/react/package.json`,
374+
);
375+
});
376+
377+
it('matches any path segment', async () => {
378+
const fixtureName = 'workspace-cyclic';
379+
const result = await parsePnpmWorkspace(
380+
__dirname,
381+
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
382+
{
383+
includeDevDeps: false,
384+
includeOptionalDeps: true,
385+
pruneWithinTopLevelDeps: true,
386+
strictOutOfSync: false,
387+
exclude: 'shared',
388+
},
389+
);
390+
391+
expect(result).toHaveLength(1);
392+
393+
const targetFiles = result.map((r) => r.targetFile.replace(/\\/g, '/'));
394+
expect(targetFiles).toContain(
395+
`fixtures/${lockFileVersionPath}/${fixtureName}/package.json`,
396+
);
397+
expect(targetFiles).not.toContain(
398+
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/backend/package.json`,
399+
);
400+
expect(targetFiles).not.toContain(
401+
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/react/package.json`,
402+
);
403+
});
404+
405+
it('handles spaces in comma-separated list', async () => {
406+
const fixtureName = 'workspace-cyclic';
407+
const result = await parsePnpmWorkspace(
408+
__dirname,
409+
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
410+
{
411+
includeDevDeps: false,
412+
includeOptionalDeps: true,
413+
pruneWithinTopLevelDeps: true,
414+
strictOutOfSync: false,
415+
exclude: ' backend , react ',
416+
},
417+
);
418+
419+
expect(result).toHaveLength(1);
420+
421+
const targetFiles = result.map((r) => r.targetFile.replace(/\\/g, '/'));
422+
expect(targetFiles).toContain(
423+
`fixtures/${lockFileVersionPath}/${fixtureName}/package.json`,
424+
);
425+
});
426+
427+
it('works normally when exclude is undefined', async () => {
428+
const fixtureName = 'workspace-cyclic';
429+
const result = await parsePnpmWorkspace(
430+
__dirname,
431+
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
432+
{
433+
includeDevDeps: false,
434+
includeOptionalDeps: true,
435+
pruneWithinTopLevelDeps: true,
436+
strictOutOfSync: false,
437+
exclude: undefined,
438+
},
439+
);
440+
441+
expect(result).toHaveLength(3);
442+
});
443+
444+
it('works normally when exclude is empty string', async () => {
445+
const fixtureName = 'workspace-cyclic';
446+
const result = await parsePnpmWorkspace(
447+
__dirname,
448+
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
449+
{
450+
includeDevDeps: false,
451+
includeOptionalDeps: true,
452+
pruneWithinTopLevelDeps: true,
453+
strictOutOfSync: false,
454+
exclude: '',
455+
},
456+
);
457+
458+
expect(result).toHaveLength(3);
459+
});
460+
},
461+
);
462+
});

0 commit comments

Comments
 (0)