Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 18 additions & 4 deletions lib/dep-graph-builders/pnpm/parse-workspace.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
import { DepGraph } from '@snyk/dep-graph';
import * as debugModule from 'debug';
import * as path from 'path';
import { parsePkgJson } from '../util';
import { getPnpmLockfileVersion } from '../../utils';
import {
PackageJsonBase,
PnpmProjectParseOptions,
ScannedNodeProject,
} from '../types';
import { parsePkgJson } from '../util';
import { buildDepGraphPnpm } from './build-dep-graph-pnpm';
import { DepGraph } from '@snyk/dep-graph';
import { UNDEFINED_VERSION } from './constants';
import { getPnpmLockfileParser } from './lockfile-parser/index';
import { PnpmLockfileParser } from './lockfile-parser/lockfile-parser';
import { getPnpmLockfileVersion } from '../../utils';
import { getFileContents } from './utils';
import { UNDEFINED_VERSION } from './constants';

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

Expand Down Expand Up @@ -55,6 +55,7 @@ export const parsePnpmWorkspace = async (
includeOptionalDeps,
strictOutOfSync,
pruneWithinTopLevelDeps,
exclude,
} = options;

const pnpmLockfileContents = getFileContents(
Expand All @@ -73,7 +74,20 @@ export const parsePnpmWorkspace = async (
Object.keys(lockFileParser.importers),
);

const excludeList = exclude ? exclude.split(',').map((s) => s.trim()) : [];

for (const importer of Object.keys(lockFileParser.importers)) {
if (excludeList.length > 0) {
const importerParts = importer.split('/');
const shouldExclude = excludeList.some((excludeName) =>
importerParts.includes(excludeName),
);

if (shouldExclude) {
debug(`Skipping excluded importer: ${importer}`);
continue;
}
}
const resolvedImporterPath = path.join(workspaceDir, importer);
const packagePath = path.join(resolvedImporterPath, 'package.json');
debug(`Processing project ${packagePath} as part of a pnpm workspace`);
Expand Down
1 change: 1 addition & 0 deletions lib/dep-graph-builders/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export type PnpmProjectParseOptions = {
includeOptionalDeps: boolean;
strictOutOfSync: boolean;
pruneWithinTopLevelDeps: boolean;
exclude?: string;
};

type NodePkgManagers = 'npm' | 'yarn' | 'pnpm';
Expand Down
147 changes: 147 additions & 0 deletions test/jest/dep-graph-builders/pnpm-workspaces.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,150 @@ describe('pnpm-lock-v9 catalogs support tests', () => {
);
});
});

describe('pnpm workspaces with exclude option', () => {
describe.each(['pnpm-lock-v5', 'pnpm-lock-v6', 'pnpm-lock-v9'])(
'%s',
(lockFileVersionPath) => {
it('excludes single directory', async () => {
const fixtureName = 'workspace-cyclic';
const result = await parsePnpmWorkspace(
__dirname,
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
{
includeDevDeps: false,
includeOptionalDeps: true,
pruneWithinTopLevelDeps: true,
strictOutOfSync: false,
exclude: 'backend',
},
);

expect(result).toHaveLength(2);

const targetFiles = result.map((r) => r.targetFile.replace(/\\/g, '/'));
expect(targetFiles).toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/package.json`,
);
expect(targetFiles).toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/react/package.json`,
);
expect(targetFiles).not.toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/backend/package.json`,
);
});

it('excludes multiple directories', async () => {
const fixtureName = 'workspace-cyclic';
const result = await parsePnpmWorkspace(
__dirname,
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
{
includeDevDeps: false,
includeOptionalDeps: true,
pruneWithinTopLevelDeps: true,
strictOutOfSync: false,
exclude: 'backend,react',
},
);

expect(result).toHaveLength(1);

const targetFiles = result.map((r) => r.targetFile.replace(/\\/g, '/'));
expect(targetFiles).toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/package.json`,
);
expect(targetFiles).not.toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/backend/package.json`,
);
expect(targetFiles).not.toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/react/package.json`,
);
});

it('matches any path segment', async () => {
const fixtureName = 'workspace-cyclic';
const result = await parsePnpmWorkspace(
__dirname,
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
{
includeDevDeps: false,
includeOptionalDeps: true,
pruneWithinTopLevelDeps: true,
strictOutOfSync: false,
exclude: 'shared',
},
);

expect(result).toHaveLength(1);

const targetFiles = result.map((r) => r.targetFile.replace(/\\/g, '/'));
expect(targetFiles).toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/package.json`,
);
expect(targetFiles).not.toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/backend/package.json`,
);
expect(targetFiles).not.toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/shared/react/package.json`,
);
});

it('handles spaces in comma-separated list', async () => {
const fixtureName = 'workspace-cyclic';
const result = await parsePnpmWorkspace(
__dirname,
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
{
includeDevDeps: false,
includeOptionalDeps: true,
pruneWithinTopLevelDeps: true,
strictOutOfSync: false,
exclude: ' backend , react ',
},
);

expect(result).toHaveLength(1);

const targetFiles = result.map((r) => r.targetFile.replace(/\\/g, '/'));
expect(targetFiles).toContain(
`fixtures/${lockFileVersionPath}/${fixtureName}/package.json`,
);
});

it('works normally when exclude is undefined', async () => {
const fixtureName = 'workspace-cyclic';
const result = await parsePnpmWorkspace(
__dirname,
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
{
includeDevDeps: false,
includeOptionalDeps: true,
pruneWithinTopLevelDeps: true,
strictOutOfSync: false,
exclude: undefined,
},
);

expect(result).toHaveLength(3);
});

it('works normally when exclude is empty string', async () => {
const fixtureName = 'workspace-cyclic';
const result = await parsePnpmWorkspace(
__dirname,
join(__dirname, `./fixtures/${lockFileVersionPath}/${fixtureName}`),
{
includeDevDeps: false,
includeOptionalDeps: true,
pruneWithinTopLevelDeps: true,
strictOutOfSync: false,
exclude: '',
},
);

expect(result).toHaveLength(3);
});
},
);
});