From 5e8e75c3de4321cbb98f902f4eae2757972d3352 Mon Sep 17 00:00:00 2001 From: dterrybd Date: Wed, 24 Jun 2026 11:27:01 -0400 Subject: [PATCH 1/8] initial npm filtering approach --- .../detectables/npm/cli/NpmCliExtractor.java | 11 +- .../npm/cli/NpmCliExtractorOptions.java | 25 ++++- .../npm/cli/parse/NpmCliParser.java | 93 ++++++++++------ .../npm/lockfile/NpmLockfileOptions.java | 21 ++++ .../lockfile/parse/NpmLockfilePackager.java | 28 ++++- .../npm/packagejson/CombinedPackageJson.java | 9 +- .../CombinedPackageJsonExtractor.java | 20 ++-- .../detectable/factory/DetectableFactory.java | 13 ++- .../npm/cli/parse/NpmCliParserTest.java | 52 +++++++++ .../lockfile/unit/NpmWorkspaceFilterTest.java | 103 ++++++++++++++++++ .../CombinedPackageJsonExtractorTest.java | 26 +++++ .../workspace-filter-test/package-lock.json | 45 ++++++++ .../npm/workspace-filter-test/package.json | 11 ++ .../packages/api/package.json | 7 ++ .../packages/ui/package.json | 4 + .../configuration/DetectProperties.java | 24 ++++ .../DetectPropertyFromVersion.java | 3 +- .../DetectableOptionFactory.java | 8 +- 18 files changed, 455 insertions(+), 48 deletions(-) create mode 100644 detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java create mode 100644 detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/package-lock.json create mode 100644 detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/package.json create mode 100644 detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/packages/api/package.json create mode 100644 detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/packages/ui/package.json diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractor.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractor.java index 012d0f1b7f..100bfb6e9a 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractor.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractor.java @@ -26,6 +26,7 @@ import com.blackduck.integration.detectable.extraction.Extraction; import com.blackduck.integration.detectable.util.ToolVersionLogger; import com.blackduck.integration.executable.ExecutableOutput; +import com.blackduck.integration.util.ExcludedIncludedWildcardFilter; public class NpmCliExtractor { public static final String OUTPUT_FILE = "detect_npm_proj_dependencies.json"; @@ -36,12 +37,20 @@ public class NpmCliExtractor { private final NpmCliParser npmCliParser; private final Gson gson; private final ToolVersionLogger toolVersionLogger; + @Nullable private final ExcludedIncludedWildcardFilter workspaceFilter; public NpmCliExtractor(DetectableExecutableRunner executableRunner, NpmCliParser npmCliParser, Gson gson, ToolVersionLogger toolVersionLogger) { + this(executableRunner, npmCliParser, gson, toolVersionLogger, null); + } + + public NpmCliExtractor(DetectableExecutableRunner executableRunner, NpmCliParser npmCliParser, + Gson gson, ToolVersionLogger toolVersionLogger, + @Nullable ExcludedIncludedWildcardFilter workspaceFilter) { this.executableRunner = executableRunner; this.npmCliParser = npmCliParser; this.gson = gson; this.toolVersionLogger = toolVersionLogger; + this.workspaceFilter = workspaceFilter; } public Extraction extract(File directory, ExecutableTarget npmExe, @Nullable String npmArguments, File packageJsonFile) { @@ -77,7 +86,7 @@ public Extraction extract(File directory, ExecutableTarget npmExe, @Nullable Str } else if (StringUtils.isNotBlank(standardOutput)) { logger.debug("Parsing npm ls file."); logger.debug(standardOutput); - NpmPackagerResult result = npmCliParser.generateCodeLocation(standardOutput, combinedPackageJson); + NpmPackagerResult result = npmCliParser.generateCodeLocation(standardOutput, combinedPackageJson, workspaceFilter); String projectName = result.getProjectName() != null ? result.getProjectName() : combinedPackageJson.getName(); String projectVersion = result.getProjectVersion() != null ? result.getProjectVersion() : combinedPackageJson.getVersion(); return new Extraction.Builder().success(result.getCodeLocation()).projectName(projectName).projectVersion(projectVersion).build(); diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractorOptions.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractorOptions.java index 7c5562c94b..a325e13af3 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractorOptions.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractorOptions.java @@ -1,5 +1,7 @@ package com.blackduck.integration.detectable.detectables.npm.cli; +import java.util.Collections; +import java.util.List; import java.util.Optional; import com.blackduck.integration.detectable.detectable.util.EnumListFilter; @@ -8,10 +10,23 @@ public class NpmCliExtractorOptions { private final EnumListFilter npmDependencyTypeFilter; private final String npmArguments; + private final List excludedWorkspaceNames; + private final List includedWorkspaceNames; - public NpmCliExtractorOptions(EnumListFilter npmDependencyTypeFilter, String npmArguments) { + public NpmCliExtractorOptions(EnumListFilter npmDependencyTypeFilter, + String npmArguments) { + this(npmDependencyTypeFilter, npmArguments, + Collections.emptyList(), Collections.emptyList()); + } + + public NpmCliExtractorOptions(EnumListFilter npmDependencyTypeFilter, + String npmArguments, + List excludedWorkspaceNames, + List includedWorkspaceNames) { this.npmDependencyTypeFilter = npmDependencyTypeFilter; this.npmArguments = npmArguments; + this.excludedWorkspaceNames = excludedWorkspaceNames; + this.includedWorkspaceNames = includedWorkspaceNames; } public EnumListFilter getDependencyTypeFilter() { @@ -21,4 +36,12 @@ public EnumListFilter getDependencyTypeFilter() { public Optional getNpmArguments() { return Optional.ofNullable(npmArguments); } + + public List getExcludedWorkspaceNames() { + return excludedWorkspaceNames; + } + + public List getIncludedWorkspaceNames() { + return includedWorkspaceNames; + } } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java index fe62ac26be..3efa96c552 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java @@ -9,6 +9,7 @@ import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.lang3.StringUtils; +import org.jetbrains.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,6 +25,7 @@ import com.blackduck.integration.detectable.detectables.npm.NpmDependencyType; import com.blackduck.integration.detectable.detectables.npm.lockfile.result.NpmPackagerResult; import com.blackduck.integration.detectable.detectables.npm.packagejson.CombinedPackageJson; +import com.blackduck.integration.util.ExcludedIncludedWildcardFilter; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; @@ -45,41 +47,45 @@ public NpmCliParser(ExternalIdFactory externalIdFactory, EnumListFilter aliasMapping = buildAliasMapping(combinedPackageJson); - populateChildren(graph, null, npmJson.getAsJsonObject(JSON_DEPENDENCIES), true, combinedPackageJson, aliasMapping); + populateChildren(graph, null, npmJson.getAsJsonObject(JSON_DEPENDENCIES), true, + combinedPackageJson, aliasMapping, workspaceFilter); ExternalId externalId = externalIdFactory.createNameVersionExternalId(Forge.NPMJS, projectName, projectVersion); CodeLocation codeLocation = new CodeLocation(graph, externalId); return new NpmPackagerResult(projectName, projectVersion, codeLocation); - } /** @@ -99,7 +105,9 @@ private Map buildAliasMapping(CombinedPackageJson combinedPackag ); } - private void populateChildren(DependencyGraph graph, Dependency parentDependency, JsonObject parentNodeChildren, boolean isRootDependency, CombinedPackageJson combinedPackageJson, Map aliasMapping) { + private void populateChildren(DependencyGraph graph, Dependency parentDependency, JsonObject parentNodeChildren, + boolean isRootDependency, CombinedPackageJson combinedPackageJson, Map aliasMapping, + @Nullable ExcludedIncludedWildcardFilter workspaceFilter) { if (parentNodeChildren == null) { return; } @@ -113,15 +121,16 @@ private void populateChildren(DependencyGraph graph, Dependency parentDependency // Transitives can be both application and dev/peer/optional dependency graphs, but Detect shouldn't be walking a dev, peer, or optional dependency tree unless it passed the filter already. return true; } - boolean excludingBecauseDev = (npmDependencyTypeFilter.shouldExclude(NpmDependencyType.DEV, combinedPackageJson.getDevDependencies()) && combinedPackageJson.getDevDependencies().containsKey( - elementEntry.getKey())); + boolean excludingBecauseDev = (npmDependencyTypeFilter.shouldExclude(NpmDependencyType.DEV, combinedPackageJson.getDevDependencies()) + && combinedPackageJson.getDevDependencies().containsKey(elementEntry.getKey())); boolean excludingBecausePeer = (npmDependencyTypeFilter.shouldExclude(NpmDependencyType.PEER, combinedPackageJson.getPeerDependencies()) && combinedPackageJson.getPeerDependencies().containsKey(elementEntry.getKey())); boolean excludingBecauseOptional = (npmDependencyTypeFilter.shouldExclude(NpmDependencyType.OPTIONAL, combinedPackageJson.getOptionalDependencies()) && combinedPackageJson.getOptionalDependencies().containsKey(elementEntry.getKey())); return !excludingBecauseDev && !excludingBecausePeer && !excludingBecauseOptional; }) - .forEach(elementEntry -> processChild(elementEntry, graph, parentDependency, isRootDependency, combinedPackageJson, aliasMapping)); + .forEach(elementEntry -> processChild(elementEntry, graph, parentDependency, isRootDependency, + combinedPackageJson, aliasMapping, workspaceFilter)); } private void processChild( @@ -130,7 +139,8 @@ private void processChild( Dependency parentDependency, boolean isRootDependency, CombinedPackageJson combinedPackageJson, - Map aliasMapping + Map aliasMapping, + @Nullable ExcludedIncludedWildcardFilter workspaceFilter ) { JsonObject element = elementEntry.getValue().getAsJsonObject(); String name = elementEntry.getKey(); @@ -150,29 +160,50 @@ private void processChild( } ExternalId externalId = externalIdFactory.createNameVersionExternalId(Forge.NPMJS, actualName, version); Dependency child = new Dependency(actualName, version, externalId); - - // Any workspace dependency is considered a direct dependency + + // Any workspace dependency is considered a direct dependency (unless filtered out) boolean directWorkspaceDependency = false; String possibleWorkspaceDependency = Optional.ofNullable(element.getAsJsonPrimitive("resolved")) - .filter(JsonPrimitive::isString) - .map(JsonPrimitive::getAsString) - .orElse(null); - + .filter(JsonPrimitive::isString) + .map(JsonPrimitive::getAsString) + .orElse(null); + + // Whether this workspace was detected but explicitly excluded by the filter + boolean workspaceExcludedByFilter = false; + if (combinedPackageJson.getRelativeWorkspaces() != null && possibleWorkspaceDependency != null) { - // workspaces under the root resolve as file../ + // workspaces under the root resolve as file../ // remove that and see if any absolute workspace paths have this subpath - String convertedPossibleWorkspaceDependency = - possibleWorkspaceDependency.replace("file:../", ""); - - directWorkspaceDependency = - combinedPackageJson.getRelativeWorkspaces().stream().anyMatch(workspace -> workspace.equals(convertedPossibleWorkspaceDependency)); + String convertedPath = possibleWorkspaceDependency.replace("file:../", ""); + boolean isWorkspace = combinedPackageJson.getRelativeWorkspaces().stream() + .anyMatch(workspace -> workspace.equals(convertedPath)); + + if (isWorkspace) { + if (workspaceFilter != null) { + // Resolve path to package name for filter check; fall back to path if no mapping + String packageName = combinedPackageJson.getWorkspaceNameToPath().entrySet().stream() + .filter(e -> e.getValue().equals(convertedPath)) + .map(Map.Entry::getKey) + .findFirst() + .orElse(convertedPath); + directWorkspaceDependency = workspaceFilter.shouldInclude(packageName); + workspaceExcludedByFilter = !directWorkspaceDependency; + } else { + directWorkspaceDependency = true; + } + } } - populateChildren(graph, child, children, directWorkspaceDependency, combinedPackageJson, aliasMapping); + // When a workspace is excluded by the filter, do not traverse its children — + // treat it as a leaf (regular dependency with no transitive graph) + if (!workspaceExcludedByFilter) { + populateChildren(graph, child, children, directWorkspaceDependency, + combinedPackageJson, aliasMapping, workspaceFilter); + } - if (isRootDependency || directWorkspaceDependency) { + if ((isRootDependency && !workspaceExcludedByFilter) || directWorkspaceDependency) { graph.addChildToRoot(child); - } else { + } else if (!workspaceExcludedByFilter) { graph.addParentWithChild(parentDependency, child); } } else { diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/NpmLockfileOptions.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/NpmLockfileOptions.java index ab3ae5e331..289c8e6e20 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/NpmLockfileOptions.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/NpmLockfileOptions.java @@ -1,17 +1,38 @@ package com.blackduck.integration.detectable.detectables.npm.lockfile; +import java.util.List; + import com.blackduck.integration.detectable.detectable.util.EnumListFilter; import com.blackduck.integration.detectable.detectables.npm.NpmDependencyType; // TODO: Identical to NpmPackageJsonParseDetectableOptions. Similar to NpmCliExtractorOptions. Common base Options class? JM-01/2022 public class NpmLockfileOptions { private final EnumListFilter npmDependencyTypeFilter; + private final List excludedWorkspaceNames; + private final List includedWorkspaceNames; public NpmLockfileOptions(EnumListFilter npmDependencyTypeFilter) { + this(npmDependencyTypeFilter, java.util.Collections.emptyList(), java.util.Collections.emptyList()); + } + + public NpmLockfileOptions( + EnumListFilter npmDependencyTypeFilter, + List excludedWorkspaceNames, + List includedWorkspaceNames) { this.npmDependencyTypeFilter = npmDependencyTypeFilter; + this.excludedWorkspaceNames = excludedWorkspaceNames; + this.includedWorkspaceNames = includedWorkspaceNames; } public EnumListFilter getNpmDependencyTypeFilter() { return npmDependencyTypeFilter; } + + public List getExcludedWorkspaceNames() { + return excludedWorkspaceNames; + } + + public List getIncludedWorkspaceNames() { + return includedWorkspaceNames; + } } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java index af160093f9..efdd317f28 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java @@ -6,6 +6,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; import org.apache.commons.lang3.StringUtils; import org.jetbrains.annotations.Nullable; @@ -22,6 +23,7 @@ import com.blackduck.integration.detectable.detectables.npm.lockfile.result.NpmPackagerResult; import com.blackduck.integration.detectable.detectables.npm.packagejson.CombinedPackageJson; import com.blackduck.integration.detectable.detectables.npm.packagejson.CombinedPackageJsonExtractor; +import com.blackduck.integration.util.ExcludedIncludedWildcardFilter; import com.blackduck.integration.util.NameVersion; public class NpmLockfilePackager { @@ -29,12 +31,21 @@ public class NpmLockfilePackager { private final ExternalIdFactory externalIdFactory; private final NpmLockFileProjectIdTransformer projectIdTransformer; private final NpmLockfileGraphTransformer graphTransformer; + @Nullable private final ExcludedIncludedWildcardFilter workspaceFilter; public NpmLockfilePackager(Gson gson, ExternalIdFactory externalIdFactory, NpmLockFileProjectIdTransformer projectIdTransformer, NpmLockfileGraphTransformer graphTransformer) { + this(gson, externalIdFactory, projectIdTransformer, graphTransformer, null); + } + + public NpmLockfilePackager(Gson gson, ExternalIdFactory externalIdFactory, + NpmLockFileProjectIdTransformer projectIdTransformer, + NpmLockfileGraphTransformer graphTransformer, + @Nullable ExcludedIncludedWildcardFilter workspaceFilter) { this.gson = gson; this.externalIdFactory = externalIdFactory; this.projectIdTransformer = projectIdTransformer; this.graphTransformer = graphTransformer; + this.workspaceFilter = workspaceFilter; } public NpmPackagerResult parseAndTransform(@Nullable String rootJsonPath, @Nullable String packageJsonText, String lockFileText) throws IOException { @@ -68,12 +79,27 @@ public NpmPackagerResult parseAndTransform(@Nullable String rootJsonPath, @Nulla : new HashMap<>(); DependencyGraph dependencyGraph = graphTransformer.transform(packageLock, project, externalDependencies, - combinedPackageJson == null ? null : combinedPackageJson.getRelativeWorkspaces(), aliasMapping); + getFilteredWorkspaces(combinedPackageJson), aliasMapping); ExternalId projectId = projectIdTransformer.transform(combinedPackageJson, packageLock); CodeLocation codeLocation = new CodeLocation(dependencyGraph, projectId); return new NpmPackagerResult(projectId.getName(), projectId.getVersion(), codeLocation); } + private List getFilteredWorkspaces(@Nullable CombinedPackageJson combinedPackageJson) { + if (combinedPackageJson == null) { + return null; + } + List workspaces = combinedPackageJson.getRelativeWorkspaces(); + if (workspaceFilter == null || workspaces.isEmpty()) { + return workspaces; + } + Map pathToName = new HashMap<>(); + combinedPackageJson.getWorkspaceNameToPath().forEach((name, path) -> pathToName.put(path, name)); + return workspaces.stream() + .filter(path -> workspaceFilter.shouldInclude(pathToName.getOrDefault(path, path))) + .collect(Collectors.toList()); + } + public String removePathInfoFromPackageName(String lockFileText) { List searchList = new ArrayList<>(Arrays.asList("/node_modules/", "node_modules/")); List replaceList = new ArrayList<>(Arrays.asList("*", "")); diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJson.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJson.java index eeafee1017..a955698df3 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJson.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJson.java @@ -1,7 +1,9 @@ package com.blackduck.integration.detectable.detectables.npm.packagejson; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; @@ -11,6 +13,7 @@ public class CombinedPackageJson { private String name; private String version; private List relativeWorkspaces = new ArrayList<>(); + private Map workspaceNameToPath = new HashMap<>(); private MultiValuedMap dependencies; private MultiValuedMap devDependencies; @@ -40,10 +43,14 @@ public MultiValuedMap getOptionalDependencies() { return optionalDependencies; } - public List getRelativeWorkspaces() { + public List getRelativeWorkspaces() { return relativeWorkspaces; } + public Map getWorkspaceNameToPath() { + return workspaceNameToPath; + } + public String getName() { return name; } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java index f17f0363cc..6f2f27fc2a 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java @@ -67,17 +67,18 @@ public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, Str // Don't try to read a file that doesn't exist. if (!Files.exists(workspaceJsonPath)) { continue; - } else { - addRelativeWorkspace(combinedPackageJson, projectRoot, convertedWorkspace); } - - String workspaceJsonString - = FileUtils.readFileToString(new File(workspaceJsonPath.toString()), StandardCharsets.UTF_8); - + + String workspaceJsonString = + FileUtils.readFileToString(new File(workspaceJsonPath.toString()), StandardCharsets.UTF_8); + PackageJson workspacePackageJson = Optional.ofNullable(workspaceJsonString) .map(content -> gson.fromJson(content, PackageJson.class)) .orElse(null); - + + String workspacePackageName = workspacePackageJson != null ? workspacePackageJson.name : null; + addRelativeWorkspace(combinedPackageJson, projectRoot, convertedWorkspace, workspacePackageName); + if (workspacePackageJson != null) { combinedPackageJson.getDependencies().putAll(workspacePackageJson.dependencies); combinedPackageJson.getDevDependencies().putAll(workspacePackageJson.devDependencies); @@ -100,7 +101,7 @@ public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, Str * replaced */ private void addRelativeWorkspace(CombinedPackageJson combinedPackageJson, String projectRoot, - String convertedWorkspace) { + String convertedWorkspace, String workspacePackageName) { int rootIndex = convertedWorkspace.indexOf(projectRoot); if (rootIndex != -1) { int packageStartIndex = rootIndex + projectRoot.length(); @@ -109,6 +110,9 @@ private void addRelativeWorkspace(CombinedPackageJson combinedPackageJson, Strin // the package-lock.json file. String relativeWorkspace = convertedWorkspace.substring(packageStartIndex).replace("\\", "/"); combinedPackageJson.getRelativeWorkspaces().add(relativeWorkspace); + if (workspacePackageName != null) { + combinedPackageJson.getWorkspaceNameToPath().put(workspacePackageName, relativeWorkspace); + } } } } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java b/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java index f27fc36ae6..c40d4fd971 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java @@ -307,6 +307,7 @@ import com.blackduck.integration.detectable.detectables.opam.lockfile.OpamLockFileExtractor; import com.blackduck.integration.detectable.detectables.opam.transform.OpamGraphTransformer; import com.blackduck.integration.detectable.util.ToolVersionLogger; +import com.blackduck.integration.util.ExcludedIncludedWildcardFilter; /* Entry point for creating detectables using most @@ -574,7 +575,11 @@ public Conan2CliDetectable createConan2CliDetectable(DetectableEnvironment envir public NpmCliDetectable createNpmCliDetectable(DetectableEnvironment environment, NpmResolver npmResolver, NpmCliExtractorOptions npmCliExtractorOptions) { NpmCliParser npmCliParser = new NpmCliParser(externalIdFactory, npmCliExtractorOptions.getDependencyTypeFilter()); - NpmCliExtractor npmCliExtractor = new NpmCliExtractor(executableRunner, npmCliParser, gson, toolVersionLogger); + ExcludedIncludedWildcardFilter workspaceFilter = + ExcludedIncludedWildcardFilter.fromCollections( + npmCliExtractorOptions.getExcludedWorkspaceNames(), + npmCliExtractorOptions.getIncludedWorkspaceNames()); + NpmCliExtractor npmCliExtractor = new NpmCliExtractor(executableRunner, npmCliParser, gson, toolVersionLogger, workspaceFilter); return new NpmCliDetectable(environment, fileFinder, npmResolver, npmCliExtractor, npmCliExtractorOptions); } @@ -1027,7 +1032,11 @@ private ConanCliExtractor conanCliExtractor(ConanCliOptions options) { private NpmLockfilePackager npmLockfilePackager(NpmLockfileOptions npmLockfileOptions) { NpmLockfileGraphTransformer npmLockfileGraphTransformer = new NpmLockfileGraphTransformer(npmLockfileOptions.getNpmDependencyTypeFilter()); - return new NpmLockfilePackager(gson, externalIdFactory, npmLockFileProjectIdTransformer(), npmLockfileGraphTransformer); + ExcludedIncludedWildcardFilter workspaceFilter = + ExcludedIncludedWildcardFilter.fromCollections( + npmLockfileOptions.getExcludedWorkspaceNames(), + npmLockfileOptions.getIncludedWorkspaceNames()); + return new NpmLockfilePackager(gson, externalIdFactory, npmLockFileProjectIdTransformer(), npmLockfileGraphTransformer, workspaceFilter); } private NpmLockFileProjectIdTransformer npmLockFileProjectIdTransformer() { diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParserTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParserTest.java index 08abd52e63..2c56f3c1e9 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParserTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParserTest.java @@ -4,18 +4,22 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertTrue; +import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import org.junit.jupiter.api.Test; import com.blackduck.integration.bdio.graph.DependencyGraph; +import com.blackduck.integration.bdio.model.Forge; import com.blackduck.integration.bdio.model.dependency.Dependency; import com.blackduck.integration.bdio.model.externalid.ExternalIdFactory; import com.blackduck.integration.detectable.detectable.util.EnumListFilter; import com.blackduck.integration.detectable.detectables.npm.NpmDependencyType; import com.blackduck.integration.detectable.detectables.npm.lockfile.result.NpmPackagerResult; import com.blackduck.integration.detectable.detectables.npm.packagejson.CombinedPackageJson; +import com.blackduck.integration.detectable.util.graph.GraphAssert; +import com.blackduck.integration.util.ExcludedIncludedWildcardFilter; class NpmCliParserTest { @@ -180,4 +184,52 @@ void testParseNpmLsWithDevDependencyAlias() { assertEquals("devpackage", dependency.getName()); assertEquals("1.0.0", dependency.getVersion()); } + + @Test + public void excludedWorkspaceIsNotTreatedAsWorkspace() { + // npm ls -json output: root project has "my-ui" workspace and "express" regular dep. + // my-ui has lodash as its own dep. + // When my-ui is excluded, lodash should NOT become a root dependency. + String npmLsOutput = "{" + + "\"name\":\"my-project\"," + + "\"version\":\"1.0.0\"," + + "\"dependencies\":{" + + "\"express\":{\"version\":\"4.18.0\"}," + + "\"my-ui\":{" + + "\"version\":\"1.0.0\"," + + "\"resolved\":\"file:../packages/ui\"," + + "\"dependencies\":{" + + "\"lodash\":{\"version\":\"4.17.21\"}" + + "}" + + "}" + + "}" + + "}"; + + CombinedPackageJson combinedPackageJson = new CombinedPackageJson(); + combinedPackageJson.setName("my-project"); + combinedPackageJson.setVersion("1.0.0"); + // relativeWorkspaces contains the path; workspaceNameToPath maps name -> path + combinedPackageJson.getRelativeWorkspaces().add("packages/ui"); + combinedPackageJson.getWorkspaceNameToPath().put("my-ui", "packages/ui"); + + EnumListFilter depFilter = EnumListFilter.excludeNone(); + NpmCliParser parser = new NpmCliParser(externalIdFactory, depFilter); + + // Exclude "my-ui" by package name + ExcludedIncludedWildcardFilter workspaceFilter = + ExcludedIncludedWildcardFilter.fromCollections( + Collections.singletonList("my-ui"), + Collections.emptyList()); + + NpmPackagerResult result = parser.generateCodeLocation(npmLsOutput, combinedPackageJson, workspaceFilter); + + DependencyGraph graph = result.getCodeLocation().getDependencyGraph(); + GraphAssert graphAssert = new GraphAssert(Forge.NPMJS, graph); + graphAssert.hasRootDependency( + externalIdFactory.createNameVersionExternalId(Forge.NPMJS, "express", "4.18.0")); + graphAssert.hasNoDependency( + externalIdFactory.createNameVersionExternalId(Forge.NPMJS, "lodash", "4.17.21")); + graphAssert.hasNoDependency( + externalIdFactory.createNameVersionExternalId(Forge.NPMJS, "my-ui", "1.0.0")); + } } diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java new file mode 100644 index 0000000000..8b5c2c94ac --- /dev/null +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java @@ -0,0 +1,103 @@ +package com.blackduck.integration.detectable.detectables.npm.lockfile.unit; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.jupiter.api.Test; + +import com.google.gson.Gson; +import com.blackduck.integration.bdio.graph.DependencyGraph; +import com.blackduck.integration.bdio.model.Forge; +import com.blackduck.integration.bdio.model.dependency.Dependency; +import com.blackduck.integration.bdio.model.externalid.ExternalId; +import com.blackduck.integration.bdio.model.externalid.ExternalIdFactory; +import com.blackduck.integration.detectable.detectables.npm.lockfile.NpmLockfileOptions; +import com.blackduck.integration.detectable.detectables.npm.lockfile.parse.NpmLockfileGraphTransformer; +import com.blackduck.integration.detectable.detectables.npm.lockfile.parse.NpmLockfilePackager; +import com.blackduck.integration.detectable.detectables.npm.lockfile.parse.NpmLockFileProjectIdTransformer; +import com.blackduck.integration.detectable.detectables.npm.lockfile.result.NpmPackagerResult; +import com.blackduck.integration.detectable.detectable.util.EnumListFilter; +import com.blackduck.integration.detectable.detectables.npm.NpmDependencyType; +import com.blackduck.integration.detectable.util.FunctionalTestFiles; +import com.blackduck.integration.detectable.util.graph.GraphAssert; +import com.blackduck.integration.util.ExcludedIncludedWildcardFilter; + +public class NpmWorkspaceFilterTest { + + // Fixture files live under src/test/resources/detectables/functional/npm/workspace-filter-test/ + // The fixture has two workspaces: + // packages/api (package name "my-api") — has express as a dependency + // packages/ui (package name "my-ui") — has lodash as a dependency + // The root package.json declares express as a direct dependency. + private static final String FIXTURE_PATH = "/npm/workspace-filter-test/package.json"; + private static final String PACKAGE_JSON = FunctionalTestFiles.asString(FIXTURE_PATH); + private static final String LOCKFILE = FunctionalTestFiles.asString("/npm/workspace-filter-test/package-lock.json"); + + @Test + public void excludedWorkspaceIsNotTreatedAsWorkspace() throws IOException { + NpmLockfileOptions options = new NpmLockfileOptions( + EnumListFilter.excludeNone(), + Arrays.asList("my-ui"), // exclude my-ui by package name + Collections.emptyList() + ); + NpmPackagerResult result = buildResult(options); + // lodash is only brought in as a workspace dep of my-ui. + // When my-ui is excluded as a workspace, lodash should not be promoted to root. + // express (from my-api workspace) should still appear as a root dep. + DependencyGraph graph = result.getCodeLocation().getDependencyGraph(); + ExternalId expressId = new ExternalIdFactory().createNameVersionExternalId(Forge.NPMJS, "express", "4.18.0"); + ExternalId lodashId = new ExternalIdFactory().createNameVersionExternalId(Forge.NPMJS, "lodash", "4.17.21"); + + boolean expressAtRoot = graph.getRootDependencies().stream() + .map(Dependency::getExternalId) + .anyMatch(expressId::equals); + boolean lodashAtRoot = graph.getRootDependencies().stream() + .map(Dependency::getExternalId) + .anyMatch(lodashId::equals); + + assertTrue(expressAtRoot, "express should be a root dependency (promoted from my-api workspace)"); + assertFalse(lodashAtRoot, "lodash should NOT be a root dependency when my-ui workspace is excluded"); + } + + @Test + public void includedWorkspaceOnlyIncludesThatWorkspace() throws IOException { + NpmLockfileOptions options = new NpmLockfileOptions( + EnumListFilter.excludeNone(), + Collections.emptyList(), + Arrays.asList("my-api") // only include my-api + ); + NpmPackagerResult result = buildResult(options); + DependencyGraph graph = result.getCodeLocation().getDependencyGraph(); + ExternalId expressId = new ExternalIdFactory().createNameVersionExternalId(Forge.NPMJS, "express", "4.18.0"); + ExternalId lodashId = new ExternalIdFactory().createNameVersionExternalId(Forge.NPMJS, "lodash", "4.17.21"); + + boolean expressAtRoot = graph.getRootDependencies().stream() + .map(Dependency::getExternalId) + .anyMatch(expressId::equals); + boolean lodashAtRoot = graph.getRootDependencies().stream() + .map(Dependency::getExternalId) + .anyMatch(lodashId::equals); + + assertTrue(expressAtRoot, "express should be a root dependency (promoted from my-api workspace)"); + assertFalse(lodashAtRoot, "lodash should NOT be a root dependency when only my-api workspace is included"); + } + + private NpmPackagerResult buildResult(NpmLockfileOptions options) throws IOException { + String rootJsonPath = FunctionalTestFiles.resolvePath(FIXTURE_PATH); + Gson gson = new Gson(); + ExternalIdFactory externalIdFactory = new ExternalIdFactory(); + NpmLockfileGraphTransformer transformer = + new NpmLockfileGraphTransformer(options.getNpmDependencyTypeFilter()); + ExcludedIncludedWildcardFilter workspaceFilter = + ExcludedIncludedWildcardFilter.fromCollections( + options.getExcludedWorkspaceNames(), + options.getIncludedWorkspaceNames()); + NpmLockfilePackager packager = + new NpmLockfilePackager(gson, externalIdFactory, + new NpmLockFileProjectIdTransformer(gson, externalIdFactory), transformer, workspaceFilter); + return packager.parseAndTransform(rootJsonPath, PACKAGE_JSON, LOCKFILE); + } +} diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/CombinedPackageJsonExtractorTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/CombinedPackageJsonExtractorTest.java index 4c95e96568..c4e3259db8 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/CombinedPackageJsonExtractorTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/CombinedPackageJsonExtractorTest.java @@ -1,13 +1,17 @@ package com.blackduck.integration.detectable.detectables.npm.packagejson.unit; +import java.io.File; import java.io.IOException; +import java.nio.charset.StandardCharsets; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; +import java.util.Map; import java.util.Map.Entry; import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; +import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,6 +20,8 @@ import com.blackduck.integration.detectable.detectables.npm.packagejson.CombinedPackageJsonExtractor; import com.blackduck.integration.detectable.util.FunctionalTestFiles; +import static org.junit.jupiter.api.Assertions.*; + public class CombinedPackageJsonExtractorTest { @Test public void testConstructCombinedPackageJsonWithWildcards() throws IOException { @@ -59,6 +65,26 @@ public void testConstructCombinedPackageJsonWithRelative() throws IOException { validateDiscoveredWorkspaceInformation(combinedPackageJson); } + @Test + public void testWorkspaceNameToPathMapIsPopulated() throws IOException { + // Use the existing dev-exclusion-workspace-test fixture. + // Its root package.json has "workspaces": ["packages/*", "packages/*/*"] + // and packages/w1/package.json has "name": "w1". + File rootDir = FunctionalTestFiles.asFile("/npm/dev-exclusion-workspace-test"); + File rootPackageJson = new File(rootDir, "package.json"); + String rootPackageJsonText = FileUtils.readFileToString(rootPackageJson, StandardCharsets.UTF_8); + + CombinedPackageJsonExtractor extractor = new CombinedPackageJsonExtractor(new Gson()); + CombinedPackageJson result = extractor.constructCombinedPackageJson( + rootPackageJson.getAbsolutePath(), rootPackageJsonText); + + assertNotNull(result); + Map nameToPath = result.getWorkspaceNameToPath(); + assertFalse(nameToPath.isEmpty(), "workspaceNameToPath should not be empty"); + assertEquals("packages/w1", nameToPath.get("w1"), + "Package name 'w1' should map to relative path 'packages/w1'"); + } + private void validateDiscoveredWorkspaceInformation(CombinedPackageJson combinedPackageJson) { // Test basic information Assertions.assertTrue(combinedPackageJson.getName().equals("npmworkspace")); diff --git a/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/package-lock.json b/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/package-lock.json new file mode 100644 index 0000000000..a79dba4614 --- /dev/null +++ b/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/package-lock.json @@ -0,0 +1,45 @@ +{ + "name": "my-project", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "my-project", + "version": "1.0.0", + "workspaces": [ + "packages/api", + "packages/ui" + ], + "dependencies": { + "express": "4.18.0" + } + }, + "node_modules/express": { + "version": "4.18.0" + }, + "node_modules/lodash": { + "version": "4.17.21" + }, + "node_modules/my-api": { + "resolved": "packages/api", + "link": true + }, + "node_modules/my-ui": { + "resolved": "packages/ui", + "link": true + }, + "packages/api": { + "version": "1.0.0", + "dependencies": { + "express": "4.18.0" + } + }, + "packages/ui": { + "version": "1.0.0", + "dependencies": { + "lodash": "4.17.21" + } + } + } +} diff --git a/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/package.json b/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/package.json new file mode 100644 index 0000000000..3c0fcede65 --- /dev/null +++ b/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/package.json @@ -0,0 +1,11 @@ +{ + "name": "my-project", + "version": "1.0.0", + "workspaces": [ + "packages/api", + "packages/ui" + ], + "dependencies": { + "express": "4.18.0" + } +} diff --git a/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/packages/api/package.json b/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/packages/api/package.json new file mode 100644 index 0000000000..7a481b81bb --- /dev/null +++ b/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/packages/api/package.json @@ -0,0 +1,7 @@ +{ + "name": "my-api", + "version": "1.0.0", + "dependencies": { + "express": "4.18.0" + } +} diff --git a/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/packages/ui/package.json b/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/packages/ui/package.json new file mode 100644 index 0000000000..a0c8321acb --- /dev/null +++ b/detectable/src/test/resources/detectables/functional/npm/workspace-filter-test/packages/ui/package.json @@ -0,0 +1,4 @@ +{ + "name": "my-ui", + "version": "1.0.0" +} diff --git a/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java b/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java index e9f5fed3d6..bd7e2a1905 100644 --- a/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java +++ b/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java @@ -1362,6 +1362,30 @@ private DetectProperties() { .setGroups(DetectGroup.NPM, DetectGroup.GLOBAL, DetectGroup.SOURCE_SCAN) .build(); + public static final CaseSensitiveStringListProperty DETECT_NPM_EXCLUDED_WORKSPACES = + CaseSensitiveStringListProperty.newBuilder("detect.npm.excluded.workspaces") + .setInfo("NPM Exclude Workspaces", DetectPropertyFromVersion.VERSION_12_0_0) + .setHelp( + "A comma-separated list of npm workspace package names to exclude.", + "By default, Detect includes all workspaces. Workspaces are identified by the 'name' field in the workspace's package.json. This property accepts filename globbing-style wildcards. For more information, refer to the Property wildcard support page." + ) + .setGroups(DetectGroup.NPM, DetectGroup.SOURCE_SCAN) + .setCategory(DetectCategory.Advanced) + .setExample("@myorg/test-harness,@myorg/internal-*") + .build(); + + public static final CaseSensitiveStringListProperty DETECT_NPM_INCLUDED_WORKSPACES = + CaseSensitiveStringListProperty.newBuilder("detect.npm.included.workspaces") + .setInfo("NPM Include Workspaces", DetectPropertyFromVersion.VERSION_12_0_0) + .setHelp( + "A comma-separated list of npm workspace package names to include.", + "By default, Detect includes all workspaces. If workspaces are excluded or included, Detect will include any workspace included by this property that is not excluded. Exclusion rules always win. This property accepts filename globbing-style wildcards. For more information, refer to the Property wildcard support page." + ) + .setGroups(DetectGroup.NPM, DetectGroup.SOURCE_SCAN) + .setCategory(DetectCategory.Advanced) + .setExample("@myorg/frontend,@myorg/api") + .build(); + public static final NullablePathProperty DETECT_NPM_PATH = NullablePathProperty.newBuilder("detect.npm.path") .setInfo("NPM Executable", DetectPropertyFromVersion.VERSION_3_0_0) diff --git a/src/main/java/com/blackduck/integration/detect/configuration/DetectPropertyFromVersion.java b/src/main/java/com/blackduck/integration/detect/configuration/DetectPropertyFromVersion.java index e447c92b8d..7142d113ee 100644 --- a/src/main/java/com/blackduck/integration/detect/configuration/DetectPropertyFromVersion.java +++ b/src/main/java/com/blackduck/integration/detect/configuration/DetectPropertyFromVersion.java @@ -60,7 +60,8 @@ public enum DetectPropertyFromVersion implements PropertyVersion { VERSION_11_0_0("11.0.0"), VERSION_11_2_0("11.2.0"), VERSION_11_3_0("11.3.0"), - VERSION_11_4_0("11.4.0"); + VERSION_11_4_0("11.4.0"), + VERSION_12_0_0("12.0.0"); private final String version; diff --git a/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java b/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java index 92ace8c329..5b2ce551ed 100644 --- a/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java +++ b/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java @@ -246,12 +246,16 @@ public ConanLockfileExtractorOptions createConanLockfileOptions() { public NpmCliExtractorOptions createNpmCliExtractorOptions() { EnumListFilter npmDependencyTypeFilter = createNpmDependencyTypeFilter(); String npmArguments = detectConfiguration.getNullableValue(DetectProperties.DETECT_NPM_ARGUMENTS); - return new NpmCliExtractorOptions(npmDependencyTypeFilter, npmArguments); + List excludedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_EXCLUDED_WORKSPACES); + List includedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_INCLUDED_WORKSPACES); + return new NpmCliExtractorOptions(npmDependencyTypeFilter, npmArguments, excludedWorkspaceNames, includedWorkspaceNames); } public NpmLockfileOptions createNpmLockfileOptions() { EnumListFilter npmDependencyTypeFilter = createNpmDependencyTypeFilter(); - return new NpmLockfileOptions(npmDependencyTypeFilter); + List excludedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_EXCLUDED_WORKSPACES); + List includedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_INCLUDED_WORKSPACES); + return new NpmLockfileOptions(npmDependencyTypeFilter, excludedWorkspaceNames, includedWorkspaceNames); } public NpmPackageJsonParseDetectableOptions createNpmPackageJsonParseDetectableOptions() { From 99fdaf3f0827de6618af0f0ce529c7f705ae4a4f Mon Sep 17 00:00:00 2001 From: dterrybd Date: Thu, 25 Jun 2026 11:06:58 -0400 Subject: [PATCH 2/8] rework filtering to be relative path based --- .../npm/cli/parse/NpmCliParser.java | 8 +----- .../lockfile/parse/NpmLockfilePackager.java | 4 +-- .../npm/packagejson/CombinedPackageJson.java | 9 +------ .../CombinedPackageJsonExtractor.java | 8 ++---- .../npm/cli/parse/NpmCliParserTest.java | 6 ++--- .../lockfile/unit/NpmWorkspaceFilterTest.java | 4 +-- .../CombinedPackageJsonExtractorTest.java | 27 ------------------- .../configuration/DetectProperties.java | 12 ++++----- 8 files changed, 15 insertions(+), 63 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java index 3efa96c552..acfb511a2e 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java @@ -180,13 +180,7 @@ private void processChild( if (isWorkspace) { if (workspaceFilter != null) { - // Resolve path to package name for filter check; fall back to path if no mapping - String packageName = combinedPackageJson.getWorkspaceNameToPath().entrySet().stream() - .filter(e -> e.getValue().equals(convertedPath)) - .map(Map.Entry::getKey) - .findFirst() - .orElse(convertedPath); - directWorkspaceDependency = workspaceFilter.shouldInclude(packageName); + directWorkspaceDependency = workspaceFilter.shouldInclude(convertedPath); workspaceExcludedByFilter = !directWorkspaceDependency; } else { directWorkspaceDependency = true; diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java index efdd317f28..61f4d2ea24 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java @@ -93,10 +93,8 @@ private List getFilteredWorkspaces(@Nullable CombinedPackageJson combine if (workspaceFilter == null || workspaces.isEmpty()) { return workspaces; } - Map pathToName = new HashMap<>(); - combinedPackageJson.getWorkspaceNameToPath().forEach((name, path) -> pathToName.put(path, name)); return workspaces.stream() - .filter(path -> workspaceFilter.shouldInclude(pathToName.getOrDefault(path, path))) + .filter(path -> workspaceFilter.shouldInclude(path)) .collect(Collectors.toList()); } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJson.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJson.java index a955698df3..f50580690b 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJson.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJson.java @@ -1,9 +1,7 @@ package com.blackduck.integration.detectable.detectables.npm.packagejson; import java.util.ArrayList; -import java.util.HashMap; import java.util.List; -import java.util.Map; import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; @@ -13,8 +11,7 @@ public class CombinedPackageJson { private String name; private String version; private List relativeWorkspaces = new ArrayList<>(); - private Map workspaceNameToPath = new HashMap<>(); - + private MultiValuedMap dependencies; private MultiValuedMap devDependencies; private MultiValuedMap peerDependencies; @@ -47,10 +44,6 @@ public List getRelativeWorkspaces() { return relativeWorkspaces; } - public Map getWorkspaceNameToPath() { - return workspaceNameToPath; - } - public String getName() { return name; } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java index 6f2f27fc2a..aa82903e7e 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java @@ -76,8 +76,7 @@ public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, Str .map(content -> gson.fromJson(content, PackageJson.class)) .orElse(null); - String workspacePackageName = workspacePackageJson != null ? workspacePackageJson.name : null; - addRelativeWorkspace(combinedPackageJson, projectRoot, convertedWorkspace, workspacePackageName); + addRelativeWorkspace(combinedPackageJson, projectRoot, convertedWorkspace); if (workspacePackageJson != null) { combinedPackageJson.getDependencies().putAll(workspacePackageJson.dependencies); @@ -101,7 +100,7 @@ public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, Str * replaced */ private void addRelativeWorkspace(CombinedPackageJson combinedPackageJson, String projectRoot, - String convertedWorkspace, String workspacePackageName) { + String convertedWorkspace) { int rootIndex = convertedWorkspace.indexOf(projectRoot); if (rootIndex != -1) { int packageStartIndex = rootIndex + projectRoot.length(); @@ -110,9 +109,6 @@ private void addRelativeWorkspace(CombinedPackageJson combinedPackageJson, Strin // the package-lock.json file. String relativeWorkspace = convertedWorkspace.substring(packageStartIndex).replace("\\", "/"); combinedPackageJson.getRelativeWorkspaces().add(relativeWorkspace); - if (workspacePackageName != null) { - combinedPackageJson.getWorkspaceNameToPath().put(workspacePackageName, relativeWorkspace); - } } } } diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParserTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParserTest.java index 2c56f3c1e9..4cc87d8bd7 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParserTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParserTest.java @@ -208,17 +208,15 @@ public void excludedWorkspaceIsNotTreatedAsWorkspace() { CombinedPackageJson combinedPackageJson = new CombinedPackageJson(); combinedPackageJson.setName("my-project"); combinedPackageJson.setVersion("1.0.0"); - // relativeWorkspaces contains the path; workspaceNameToPath maps name -> path combinedPackageJson.getRelativeWorkspaces().add("packages/ui"); - combinedPackageJson.getWorkspaceNameToPath().put("my-ui", "packages/ui"); EnumListFilter depFilter = EnumListFilter.excludeNone(); NpmCliParser parser = new NpmCliParser(externalIdFactory, depFilter); - // Exclude "my-ui" by package name + // Exclude by relative path ExcludedIncludedWildcardFilter workspaceFilter = ExcludedIncludedWildcardFilter.fromCollections( - Collections.singletonList("my-ui"), + Collections.singletonList("packages/ui"), Collections.emptyList()); NpmPackagerResult result = parser.generateCodeLocation(npmLsOutput, combinedPackageJson, workspaceFilter); diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java index 8b5c2c94ac..bb4a12455f 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java @@ -40,7 +40,7 @@ public class NpmWorkspaceFilterTest { public void excludedWorkspaceIsNotTreatedAsWorkspace() throws IOException { NpmLockfileOptions options = new NpmLockfileOptions( EnumListFilter.excludeNone(), - Arrays.asList("my-ui"), // exclude my-ui by package name + Arrays.asList("packages/ui"), // exclude by relative path Collections.emptyList() ); NpmPackagerResult result = buildResult(options); @@ -67,7 +67,7 @@ public void includedWorkspaceOnlyIncludesThatWorkspace() throws IOException { NpmLockfileOptions options = new NpmLockfileOptions( EnumListFilter.excludeNone(), Collections.emptyList(), - Arrays.asList("my-api") // only include my-api + Arrays.asList("packages/api") // only include by relative path ); NpmPackagerResult result = buildResult(options); DependencyGraph graph = result.getCodeLocation().getDependencyGraph(); diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/CombinedPackageJsonExtractorTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/CombinedPackageJsonExtractorTest.java index c4e3259db8..d84f619f96 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/CombinedPackageJsonExtractorTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/CombinedPackageJsonExtractorTest.java @@ -1,17 +1,12 @@ package com.blackduck.integration.detectable.detectables.npm.packagejson.unit; -import java.io.File; import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Path; import java.nio.file.Paths; import java.util.Collection; -import java.util.Map; import java.util.Map.Entry; import org.apache.commons.collections4.MultiValuedMap; import org.apache.commons.collections4.multimap.HashSetValuedHashMap; -import org.apache.commons.io.FileUtils; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -20,8 +15,6 @@ import com.blackduck.integration.detectable.detectables.npm.packagejson.CombinedPackageJsonExtractor; import com.blackduck.integration.detectable.util.FunctionalTestFiles; -import static org.junit.jupiter.api.Assertions.*; - public class CombinedPackageJsonExtractorTest { @Test public void testConstructCombinedPackageJsonWithWildcards() throws IOException { @@ -65,26 +58,6 @@ public void testConstructCombinedPackageJsonWithRelative() throws IOException { validateDiscoveredWorkspaceInformation(combinedPackageJson); } - @Test - public void testWorkspaceNameToPathMapIsPopulated() throws IOException { - // Use the existing dev-exclusion-workspace-test fixture. - // Its root package.json has "workspaces": ["packages/*", "packages/*/*"] - // and packages/w1/package.json has "name": "w1". - File rootDir = FunctionalTestFiles.asFile("/npm/dev-exclusion-workspace-test"); - File rootPackageJson = new File(rootDir, "package.json"); - String rootPackageJsonText = FileUtils.readFileToString(rootPackageJson, StandardCharsets.UTF_8); - - CombinedPackageJsonExtractor extractor = new CombinedPackageJsonExtractor(new Gson()); - CombinedPackageJson result = extractor.constructCombinedPackageJson( - rootPackageJson.getAbsolutePath(), rootPackageJsonText); - - assertNotNull(result); - Map nameToPath = result.getWorkspaceNameToPath(); - assertFalse(nameToPath.isEmpty(), "workspaceNameToPath should not be empty"); - assertEquals("packages/w1", nameToPath.get("w1"), - "Package name 'w1' should map to relative path 'packages/w1'"); - } - private void validateDiscoveredWorkspaceInformation(CombinedPackageJson combinedPackageJson) { // Test basic information Assertions.assertTrue(combinedPackageJson.getName().equals("npmworkspace")); diff --git a/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java b/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java index bd7e2a1905..733fabc384 100644 --- a/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java +++ b/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java @@ -1366,24 +1366,24 @@ private DetectProperties() { CaseSensitiveStringListProperty.newBuilder("detect.npm.excluded.workspaces") .setInfo("NPM Exclude Workspaces", DetectPropertyFromVersion.VERSION_12_0_0) .setHelp( - "A comma-separated list of npm workspace package names to exclude.", - "By default, Detect includes all workspaces. Workspaces are identified by the 'name' field in the workspace's package.json. This property accepts filename globbing-style wildcards. For more information, refer to the Property wildcard support page." + "A comma-separated list of npm workspace relative paths to exclude.", + "By default, Detect includes all workspaces. Workspaces are identified by their path relative to the project root (e.g. packages/react-components). This property accepts filename globbing-style wildcards. For more information, refer to the Property wildcard support page." ) .setGroups(DetectGroup.NPM, DetectGroup.SOURCE_SCAN) .setCategory(DetectCategory.Advanced) - .setExample("@myorg/test-harness,@myorg/internal-*") + .setExample("packages/test-harness,packages/internal-*") .build(); public static final CaseSensitiveStringListProperty DETECT_NPM_INCLUDED_WORKSPACES = CaseSensitiveStringListProperty.newBuilder("detect.npm.included.workspaces") .setInfo("NPM Include Workspaces", DetectPropertyFromVersion.VERSION_12_0_0) .setHelp( - "A comma-separated list of npm workspace package names to include.", - "By default, Detect includes all workspaces. If workspaces are excluded or included, Detect will include any workspace included by this property that is not excluded. Exclusion rules always win. This property accepts filename globbing-style wildcards. For more information, refer to the Property wildcard support page." + "A comma-separated list of npm workspace relative paths to include.", + "By default, Detect includes all workspaces. If workspaces are excluded or included, Detect will include any workspace included by this property that is not excluded. Exclusion rules always win. Workspaces are identified by their path relative to the project root (e.g. packages/react-components). This property accepts filename globbing-style wildcards. For more information, refer to the Property wildcard support page." ) .setGroups(DetectGroup.NPM, DetectGroup.SOURCE_SCAN) .setCategory(DetectCategory.Advanced) - .setExample("@myorg/frontend,@myorg/api") + .setExample("packages/frontend,packages/api") .build(); public static final NullablePathProperty DETECT_NPM_PATH = From 8858c71b2cb944aebd37ee1bc4a75be2fb41efac Mon Sep 17 00:00:00 2001 From: dterrybd Date: Thu, 25 Jun 2026 11:45:25 -0400 Subject: [PATCH 3/8] fix cli filtering --- .../detectable/detectables/npm/cli/parse/NpmCliParser.java | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java index acfb511a2e..f3bf82b681 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/parse/NpmCliParser.java @@ -172,9 +172,9 @@ private void processChild( boolean workspaceExcludedByFilter = false; if (combinedPackageJson.getRelativeWorkspaces() != null && possibleWorkspaceDependency != null) { - // workspaces under the root resolve as file../ - // remove that and see if any absolute workspace paths have this subpath - String convertedPath = possibleWorkspaceDependency.replace("file:../", ""); + // Strip "file:" prefix and any leading "../" or "./" segments; the remaining + // path is relative to the project root, matching entries in relativeWorkspaces. + String convertedPath = possibleWorkspaceDependency.replaceFirst("^file:(\\.\\./|\\./)*", ""); boolean isWorkspace = combinedPackageJson.getRelativeWorkspaces().stream() .anyMatch(workspace -> workspace.equals(convertedPath)); From 726165630676b5addb78d5639a17f2ac18f43c10 Mon Sep 17 00:00:00 2001 From: dterrybd Date: Fri, 26 Jun 2026 09:44:27 -0400 Subject: [PATCH 4/8] fix lockfile parsing --- .../detectables/npm/cli/NpmCliExtractor.java | 2 +- .../lockfile/parse/NpmLockfilePackager.java | 2 +- .../CombinedPackageJsonExtractor.java | 46 +++++++++++++------ 3 files changed, 35 insertions(+), 15 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractor.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractor.java index 100bfb6e9a..33fe07158d 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractor.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractor.java @@ -100,7 +100,7 @@ private CombinedPackageJson parsePackageJson(File packageJson) throws IOExceptio String packageJsonText = FileUtils.readFileToString(packageJson, StandardCharsets.UTF_8); CombinedPackageJsonExtractor extractor = new CombinedPackageJsonExtractor(gson); - CombinedPackageJson combinedPackageJson = extractor.constructCombinedPackageJson(packageJson.getPath(), packageJsonText); + CombinedPackageJson combinedPackageJson = extractor.constructCombinedPackageJson(packageJson.getPath(), packageJsonText, workspaceFilter); return combinedPackageJson; } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java index 61f4d2ea24..cd45145db1 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/parse/NpmLockfilePackager.java @@ -54,7 +54,7 @@ public NpmPackagerResult parseAndTransform(@Nullable String rootJsonPath, @Nulla public NpmPackagerResult parseAndTransform(@Nullable String rootJsonPath, @Nullable String packageJsonText, String lockFileText, List externalDependencies) throws IOException { CombinedPackageJsonExtractor extractor = new CombinedPackageJsonExtractor(gson); - CombinedPackageJson combinedPackageJson = extractor.constructCombinedPackageJson(rootJsonPath, packageJsonText); + CombinedPackageJson combinedPackageJson = extractor.constructCombinedPackageJson(rootJsonPath, packageJsonText, workspaceFilter); lockFileText = removePathInfoFromPackageName(lockFileText); diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java index aa82903e7e..bc9a490195 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java @@ -11,6 +11,7 @@ import java.util.Optional; import com.blackduck.integration.detectable.util.JsonSanitizer; +import com.blackduck.integration.util.ExcludedIncludedWildcardFilter; import com.google.gson.Gson; import org.apache.commons.io.FileUtils; @@ -28,47 +29,59 @@ public CombinedPackageJsonExtractor(Gson gson) { * Merge the root package.json with any potential workspace package.json files. */ public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, String packageJsonText) throws IOException { + return constructCombinedPackageJson(rootJsonPath, packageJsonText, null); + } + + public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, String packageJsonText, + ExcludedIncludedWildcardFilter workspaceFilter) throws IOException { if (packageJsonText == null) { return null; } - + PackageJson packageJson = Optional.ofNullable(JsonSanitizer.sanitize(packageJsonText)) .map(content -> gson.fromJson(content, PackageJson.class)) .orElse(null); - + if (packageJson == null) { return null; } - + CombinedPackageJson combinedPackageJson = new CombinedPackageJson(); - + // Take fields that will be related to BD projects from the root project.json combinedPackageJson.setName(packageJson.name); combinedPackageJson.setVersion(packageJson.version); - + // Add dependencies from the root of the project combinedPackageJson.getDependencies().putAll(packageJson.dependencies); combinedPackageJson.getDevDependencies().putAll(packageJson.devDependencies); combinedPackageJson.getPeerDependencies().putAll(packageJson.peerDependencies); combinedPackageJson.getOptionalDependencies().putAll(packageJson.optionalDependencies); - + if (packageJson.workspaces != null && rootJsonPath != null) { // If there are workspaces there are additional package.json's we need to parse String projectRoot = rootJsonPath.substring(0, rootJsonPath.lastIndexOf(File.separator) + 1); - - List convertedWorkspaces = + + List convertedWorkspaces = convertWorkspaceWildcards(projectRoot, packageJson.workspaces); - + for(String convertedWorkspace : convertedWorkspaces) { Path workspaceJsonPath = Paths.get(convertedWorkspace + "/package.json").normalize(); - + // We are looking for a package.json but they aren't always where we expect them. // Don't try to read a file that doesn't exist. if (!Files.exists(workspaceJsonPath)) { continue; } + // Skip excluded workspaces before merging their deps into the combined package.json. + // This prevents excluded workspace deps from appearing as declared root dependencies. + String relativePath = getRelativePath(projectRoot, convertedWorkspace); + if (workspaceFilter != null && relativePath != null && !workspaceFilter.shouldInclude(relativePath)) { + continue; + } + String workspaceJsonString = FileUtils.readFileToString(new File(workspaceJsonPath.toString()), StandardCharsets.UTF_8); @@ -86,7 +99,7 @@ public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, Str } } } - + return combinedPackageJson; } @@ -101,16 +114,23 @@ public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, Str */ private void addRelativeWorkspace(CombinedPackageJson combinedPackageJson, String projectRoot, String convertedWorkspace) { + String relativePath = getRelativePath(projectRoot, convertedWorkspace); + if (relativePath != null) { + combinedPackageJson.getRelativeWorkspaces().add(relativePath); + } + } + + private String getRelativePath(String projectRoot, String convertedWorkspace) { int rootIndex = convertedWorkspace.indexOf(projectRoot); if (rootIndex != -1) { int packageStartIndex = rootIndex + projectRoot.length(); if (packageStartIndex < convertedWorkspace.length()) { // Replace any \'s with /'s, so we can properly compare workspace names with what is in // the package-lock.json file. - String relativeWorkspace = convertedWorkspace.substring(packageStartIndex).replace("\\", "/"); - combinedPackageJson.getRelativeWorkspaces().add(relativeWorkspace); + return convertedWorkspace.substring(packageStartIndex).replace("\\", "/"); } } + return null; } /** From e88301c747278c364585953f9af9af8239025c20 Mon Sep 17 00:00:00 2001 From: dterrybd Date: Wed, 1 Jul 2026 13:11:49 -0400 Subject: [PATCH 5/8] add ignore all workspaces flag --- .../npm/cli/NpmCliExtractorOptions.java | 16 +++++++++++++++- .../npm/lockfile/NpmLockfileOptions.java | 16 +++++++++++++++- .../detectable/factory/DetectableFactory.java | 17 +++++++++++++---- .../detect/configuration/DetectProperties.java | 7 +++++++ .../configuration/DetectableOptionFactory.java | 6 ++++-- 5 files changed, 54 insertions(+), 8 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractorOptions.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractorOptions.java index a325e13af3..41e82ad3db 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractorOptions.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/cli/NpmCliExtractorOptions.java @@ -12,21 +12,31 @@ public class NpmCliExtractorOptions { private final String npmArguments; private final List excludedWorkspaceNames; private final List includedWorkspaceNames; + private final boolean ignoreAllWorkspaces; public NpmCliExtractorOptions(EnumListFilter npmDependencyTypeFilter, String npmArguments) { this(npmDependencyTypeFilter, npmArguments, - Collections.emptyList(), Collections.emptyList()); + Collections.emptyList(), Collections.emptyList(), false); } public NpmCliExtractorOptions(EnumListFilter npmDependencyTypeFilter, String npmArguments, List excludedWorkspaceNames, List includedWorkspaceNames) { + this(npmDependencyTypeFilter, npmArguments, excludedWorkspaceNames, includedWorkspaceNames, false); + } + + public NpmCliExtractorOptions(EnumListFilter npmDependencyTypeFilter, + String npmArguments, + List excludedWorkspaceNames, + List includedWorkspaceNames, + boolean ignoreAllWorkspaces) { this.npmDependencyTypeFilter = npmDependencyTypeFilter; this.npmArguments = npmArguments; this.excludedWorkspaceNames = excludedWorkspaceNames; this.includedWorkspaceNames = includedWorkspaceNames; + this.ignoreAllWorkspaces = ignoreAllWorkspaces; } public EnumListFilter getDependencyTypeFilter() { @@ -44,4 +54,8 @@ public List getExcludedWorkspaceNames() { public List getIncludedWorkspaceNames() { return includedWorkspaceNames; } + + public boolean isIgnoreAllWorkspaces() { + return ignoreAllWorkspaces; + } } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/NpmLockfileOptions.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/NpmLockfileOptions.java index 289c8e6e20..28e50c287a 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/NpmLockfileOptions.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/lockfile/NpmLockfileOptions.java @@ -10,18 +10,28 @@ public class NpmLockfileOptions { private final EnumListFilter npmDependencyTypeFilter; private final List excludedWorkspaceNames; private final List includedWorkspaceNames; + private final boolean ignoreAllWorkspaces; public NpmLockfileOptions(EnumListFilter npmDependencyTypeFilter) { - this(npmDependencyTypeFilter, java.util.Collections.emptyList(), java.util.Collections.emptyList()); + this(npmDependencyTypeFilter, java.util.Collections.emptyList(), java.util.Collections.emptyList(), false); } public NpmLockfileOptions( EnumListFilter npmDependencyTypeFilter, List excludedWorkspaceNames, List includedWorkspaceNames) { + this(npmDependencyTypeFilter, excludedWorkspaceNames, includedWorkspaceNames, false); + } + + public NpmLockfileOptions( + EnumListFilter npmDependencyTypeFilter, + List excludedWorkspaceNames, + List includedWorkspaceNames, + boolean ignoreAllWorkspaces) { this.npmDependencyTypeFilter = npmDependencyTypeFilter; this.excludedWorkspaceNames = excludedWorkspaceNames; this.includedWorkspaceNames = includedWorkspaceNames; + this.ignoreAllWorkspaces = ignoreAllWorkspaces; } public EnumListFilter getNpmDependencyTypeFilter() { @@ -35,4 +45,8 @@ public List getExcludedWorkspaceNames() { public List getIncludedWorkspaceNames() { return includedWorkspaceNames; } + + public boolean isIgnoreAllWorkspaces() { + return ignoreAllWorkspaces; + } } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java b/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java index c40d4fd971..4013dd80f5 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java @@ -1,6 +1,8 @@ package com.blackduck.integration.detectable.factory; import java.io.File; +import java.util.Collections; +import java.util.List; import javax.xml.parsers.ParserConfigurationException; import javax.xml.parsers.SAXParser; @@ -575,8 +577,8 @@ public Conan2CliDetectable createConan2CliDetectable(DetectableEnvironment envir public NpmCliDetectable createNpmCliDetectable(DetectableEnvironment environment, NpmResolver npmResolver, NpmCliExtractorOptions npmCliExtractorOptions) { NpmCliParser npmCliParser = new NpmCliParser(externalIdFactory, npmCliExtractorOptions.getDependencyTypeFilter()); - ExcludedIncludedWildcardFilter workspaceFilter = - ExcludedIncludedWildcardFilter.fromCollections( + ExcludedIncludedWildcardFilter workspaceFilter = buildNpmWorkspaceFilter( + npmCliExtractorOptions.isIgnoreAllWorkspaces(), npmCliExtractorOptions.getExcludedWorkspaceNames(), npmCliExtractorOptions.getIncludedWorkspaceNames()); NpmCliExtractor npmCliExtractor = new NpmCliExtractor(executableRunner, npmCliParser, gson, toolVersionLogger, workspaceFilter); @@ -1032,13 +1034,20 @@ private ConanCliExtractor conanCliExtractor(ConanCliOptions options) { private NpmLockfilePackager npmLockfilePackager(NpmLockfileOptions npmLockfileOptions) { NpmLockfileGraphTransformer npmLockfileGraphTransformer = new NpmLockfileGraphTransformer(npmLockfileOptions.getNpmDependencyTypeFilter()); - ExcludedIncludedWildcardFilter workspaceFilter = - ExcludedIncludedWildcardFilter.fromCollections( + ExcludedIncludedWildcardFilter workspaceFilter = buildNpmWorkspaceFilter( + npmLockfileOptions.isIgnoreAllWorkspaces(), npmLockfileOptions.getExcludedWorkspaceNames(), npmLockfileOptions.getIncludedWorkspaceNames()); return new NpmLockfilePackager(gson, externalIdFactory, npmLockFileProjectIdTransformer(), npmLockfileGraphTransformer, workspaceFilter); } + private ExcludedIncludedWildcardFilter buildNpmWorkspaceFilter(boolean ignoreAllWorkspaces, List excluded, List included) { + if (ignoreAllWorkspaces) { + return ExcludedIncludedWildcardFilter.fromCollections(Collections.singletonList("*"), Collections.emptyList()); + } + return ExcludedIncludedWildcardFilter.fromCollections(excluded, included); + } + private NpmLockFileProjectIdTransformer npmLockFileProjectIdTransformer() { return new NpmLockFileProjectIdTransformer(gson, externalIdFactory); } diff --git a/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java b/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java index 733fabc384..cee570df05 100644 --- a/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java +++ b/src/main/java/com/blackduck/integration/detect/configuration/DetectProperties.java @@ -1386,6 +1386,13 @@ private DetectProperties() { .setExample("packages/frontend,packages/api") .build(); + public static final BooleanProperty DETECT_NPM_IGNORE_ALL_WORKSPACES_MODE = + BooleanProperty.newBuilder("detect.npm.ignore.all.workspaces", false) + .setInfo("Ignore All Workspaces", DetectPropertyFromVersion.VERSION_12_0_0) + .setHelp("All workspaces are ignored by the NPM detector for increased performance and precision to scan a massive codebase.") + .setGroups(DetectGroup.NPM, DetectGroup.SOURCE_SCAN) + .build(); + public static final NullablePathProperty DETECT_NPM_PATH = NullablePathProperty.newBuilder("detect.npm.path") .setInfo("NPM Executable", DetectPropertyFromVersion.VERSION_3_0_0) diff --git a/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java b/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java index 5b2ce551ed..511039098d 100644 --- a/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java +++ b/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java @@ -248,14 +248,16 @@ public NpmCliExtractorOptions createNpmCliExtractorOptions() { String npmArguments = detectConfiguration.getNullableValue(DetectProperties.DETECT_NPM_ARGUMENTS); List excludedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_EXCLUDED_WORKSPACES); List includedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_INCLUDED_WORKSPACES); - return new NpmCliExtractorOptions(npmDependencyTypeFilter, npmArguments, excludedWorkspaceNames, includedWorkspaceNames); + boolean ignoreAllWorkspaces = detectConfiguration.getValue(DetectProperties.DETECT_NPM_IGNORE_ALL_WORKSPACES_MODE); + return new NpmCliExtractorOptions(npmDependencyTypeFilter, npmArguments, excludedWorkspaceNames, includedWorkspaceNames, ignoreAllWorkspaces); } public NpmLockfileOptions createNpmLockfileOptions() { EnumListFilter npmDependencyTypeFilter = createNpmDependencyTypeFilter(); List excludedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_EXCLUDED_WORKSPACES); List includedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_INCLUDED_WORKSPACES); - return new NpmLockfileOptions(npmDependencyTypeFilter, excludedWorkspaceNames, includedWorkspaceNames); + boolean ignoreAllWorkspaces = detectConfiguration.getValue(DetectProperties.DETECT_NPM_IGNORE_ALL_WORKSPACES_MODE); + return new NpmLockfileOptions(npmDependencyTypeFilter, excludedWorkspaceNames, includedWorkspaceNames, ignoreAllWorkspaces); } public NpmPackageJsonParseDetectableOptions createNpmPackageJsonParseDetectableOptions() { From 338df29cbff62090ad0b0149c8e598883d5271cf Mon Sep 17 00:00:00 2001 From: dterrybd Date: Wed, 1 Jul 2026 13:21:30 -0400 Subject: [PATCH 6/8] add unit test for all filtering --- .../lockfile/unit/NpmWorkspaceFilterTest.java | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java index bb4a12455f..0ea8d03910 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/lockfile/unit/NpmWorkspaceFilterTest.java @@ -85,16 +85,39 @@ public void includedWorkspaceOnlyIncludesThatWorkspace() throws IOException { assertFalse(lodashAtRoot, "lodash should NOT be a root dependency when only my-api workspace is included"); } + @Test + public void ignoreAllWorkspacesExcludesAllWorkspaceDeps() throws IOException { + NpmLockfileOptions options = new NpmLockfileOptions( + EnumListFilter.excludeNone(), + Collections.emptyList(), + Collections.emptyList(), + true + ); + NpmPackagerResult result = buildResult(options); + DependencyGraph graph = result.getCodeLocation().getDependencyGraph(); + ExternalId expressId = new ExternalIdFactory().createNameVersionExternalId(Forge.NPMJS, "express", "4.18.0"); + ExternalId lodashId = new ExternalIdFactory().createNameVersionExternalId(Forge.NPMJS, "lodash", "4.17.21"); + + boolean expressAtRoot = graph.getRootDependencies().stream() + .map(Dependency::getExternalId) + .anyMatch(expressId::equals); + boolean lodashAtRoot = graph.getRootDependencies().stream() + .map(Dependency::getExternalId) + .anyMatch(lodashId::equals); + + assertTrue(expressAtRoot, "express should be a root dependency (declared in root package.json)"); + assertFalse(lodashAtRoot, "lodash should NOT be a root dependency when all workspaces are ignored"); + } + private NpmPackagerResult buildResult(NpmLockfileOptions options) throws IOException { String rootJsonPath = FunctionalTestFiles.resolvePath(FIXTURE_PATH); Gson gson = new Gson(); ExternalIdFactory externalIdFactory = new ExternalIdFactory(); NpmLockfileGraphTransformer transformer = new NpmLockfileGraphTransformer(options.getNpmDependencyTypeFilter()); - ExcludedIncludedWildcardFilter workspaceFilter = - ExcludedIncludedWildcardFilter.fromCollections( - options.getExcludedWorkspaceNames(), - options.getIncludedWorkspaceNames()); + ExcludedIncludedWildcardFilter workspaceFilter = options.isIgnoreAllWorkspaces() + ? ExcludedIncludedWildcardFilter.fromCollections(Collections.singletonList("*"), Collections.emptyList()) + : ExcludedIncludedWildcardFilter.fromCollections(options.getExcludedWorkspaceNames(), options.getIncludedWorkspaceNames()); NpmLockfilePackager packager = new NpmLockfilePackager(gson, externalIdFactory, new NpmLockFileProjectIdTransformer(gson, externalIdFactory), transformer, workspaceFilter); From 5dfd7d9a2affc89f3a7c1e17c1fab45516ce366a Mon Sep 17 00:00:00 2001 From: dterrybd Date: Wed, 1 Jul 2026 15:02:12 -0400 Subject: [PATCH 7/8] fix filtering for cli detector --- .../npm/packagejson/CombinedPackageJsonExtractor.java | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java index bc9a490195..d6cc397d2d 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/CombinedPackageJsonExtractor.java @@ -75,7 +75,13 @@ public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, Str continue; } - // Skip excluded workspaces before merging their deps into the combined package.json. + // Always register the workspace path so downstream code (CLI parser, lockfile packager) + // can identify workspace packages — even ones that will be excluded. Without this, + // the CLI parser can't set workspaceExcludedByFilter and the package slips through as + // a regular root dependency. + addRelativeWorkspace(combinedPackageJson, projectRoot, convertedWorkspace); + + // Skip merging deps from excluded workspaces into the combined package.json. // This prevents excluded workspace deps from appearing as declared root dependencies. String relativePath = getRelativePath(projectRoot, convertedWorkspace); if (workspaceFilter != null && relativePath != null && !workspaceFilter.shouldInclude(relativePath)) { @@ -89,8 +95,6 @@ public CombinedPackageJson constructCombinedPackageJson(String rootJsonPath, Str .map(content -> gson.fromJson(content, PackageJson.class)) .orElse(null); - addRelativeWorkspace(combinedPackageJson, projectRoot, convertedWorkspace); - if (workspacePackageJson != null) { combinedPackageJson.getDependencies().putAll(workspacePackageJson.dependencies); combinedPackageJson.getDevDependencies().putAll(workspacePackageJson.devDependencies); From e11c8c8b34027d79fa0fe4496becf03a30167a10 Mon Sep 17 00:00:00 2001 From: dterrybd Date: Wed, 1 Jul 2026 15:12:22 -0400 Subject: [PATCH 8/8] add filtering for npm package json detector --- .../NpmPackageJsonParseDetectableOptions.java | 29 +++++++ .../npm/packagejson/PackageJsonExtractor.java | 13 +++- .../detectable/factory/DetectableFactory.java | 6 +- .../unit/PackageJsonExtractorTest.java | 77 +++++++++++++++++++ .../DetectableOptionFactory.java | 5 +- 5 files changed, 127 insertions(+), 3 deletions(-) diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/NpmPackageJsonParseDetectableOptions.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/NpmPackageJsonParseDetectableOptions.java index a4be77e7fc..088306dfb4 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/NpmPackageJsonParseDetectableOptions.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/NpmPackageJsonParseDetectableOptions.java @@ -1,16 +1,45 @@ package com.blackduck.integration.detectable.detectables.npm.packagejson; +import java.util.Collections; +import java.util.List; + import com.blackduck.integration.detectable.detectable.util.EnumListFilter; import com.blackduck.integration.detectable.detectables.npm.NpmDependencyType; public class NpmPackageJsonParseDetectableOptions { private final EnumListFilter npmDependencyTypeFilter; + private final List excludedWorkspaceNames; + private final List includedWorkspaceNames; + private final boolean ignoreAllWorkspaces; public NpmPackageJsonParseDetectableOptions(EnumListFilter npmDependencyTypeFilter) { + this(npmDependencyTypeFilter, Collections.emptyList(), Collections.emptyList(), false); + } + + public NpmPackageJsonParseDetectableOptions( + EnumListFilter npmDependencyTypeFilter, + List excludedWorkspaceNames, + List includedWorkspaceNames, + boolean ignoreAllWorkspaces) { this.npmDependencyTypeFilter = npmDependencyTypeFilter; + this.excludedWorkspaceNames = excludedWorkspaceNames; + this.includedWorkspaceNames = includedWorkspaceNames; + this.ignoreAllWorkspaces = ignoreAllWorkspaces; } public EnumListFilter getNpmDependencyTypeFilter() { return npmDependencyTypeFilter; } + + public List getExcludedWorkspaceNames() { + return excludedWorkspaceNames; + } + + public List getIncludedWorkspaceNames() { + return includedWorkspaceNames; + } + + public boolean isIgnoreAllWorkspaces() { + return ignoreAllWorkspaces; + } } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/PackageJsonExtractor.java b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/PackageJsonExtractor.java index 4276fa3f57..d6700a9be5 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/PackageJsonExtractor.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/detectables/npm/packagejson/PackageJsonExtractor.java @@ -19,22 +19,33 @@ import com.blackduck.integration.bdio.model.externalid.ExternalId; import com.blackduck.integration.bdio.model.externalid.ExternalIdFactory; import com.blackduck.integration.detectable.detectable.codelocation.CodeLocation; +import org.jetbrains.annotations.Nullable; + import com.blackduck.integration.detectable.detectable.util.EnumListFilter; import com.blackduck.integration.detectable.detectable.util.SemVerComparator; import com.blackduck.integration.detectable.detectables.npm.NpmAliasParser; import com.blackduck.integration.detectable.detectables.npm.NpmDependencyType; import com.blackduck.integration.detectable.extraction.Extraction; +import com.blackduck.integration.util.ExcludedIncludedWildcardFilter; import com.google.gson.Gson; public class PackageJsonExtractor { private final Gson gson; private final ExternalIdFactory externalIdFactory; private final EnumListFilter npmDependencyTypeFilter; + @Nullable private final ExcludedIncludedWildcardFilter workspaceFilter; public PackageJsonExtractor(Gson gson, ExternalIdFactory externalIdFactory, EnumListFilter npmDependencyTypeFilter) { + this(gson, externalIdFactory, npmDependencyTypeFilter, null); + } + + public PackageJsonExtractor(Gson gson, ExternalIdFactory externalIdFactory, + EnumListFilter npmDependencyTypeFilter, + @Nullable ExcludedIncludedWildcardFilter workspaceFilter) { this.gson = gson; this.externalIdFactory = externalIdFactory; this.npmDependencyTypeFilter = npmDependencyTypeFilter; + this.workspaceFilter = workspaceFilter; } public Extraction extract(File packageJsonFile) throws IOException { @@ -46,7 +57,7 @@ public Extraction extract(File packageJsonFile) throws IOException { } CombinedPackageJsonExtractor extractor = new CombinedPackageJsonExtractor(gson); - CombinedPackageJson combinedPackageJson = extractor.constructCombinedPackageJson(packagePath, packageText); + CombinedPackageJson combinedPackageJson = extractor.constructCombinedPackageJson(packagePath, packageText, workspaceFilter); return extract(combinedPackageJson); } diff --git a/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java b/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java index 4013dd80f5..c210c9402e 100644 --- a/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java +++ b/detectable/src/main/java/com/blackduck/integration/detectable/factory/DetectableFactory.java @@ -602,7 +602,11 @@ public NpmShrinkwrapDetectable createNpmShrinkwrapDetectable(DetectableEnvironme } public NpmPackageJsonParseDetectable createNpmPackageJsonParseDetectable(DetectableEnvironment environment, NpmPackageJsonParseDetectableOptions npmPackageJsonOptions) { - PackageJsonExtractor packageJsonExtractor = new PackageJsonExtractor(gson, externalIdFactory, npmPackageJsonOptions.getNpmDependencyTypeFilter()); + ExcludedIncludedWildcardFilter workspaceFilter = buildNpmWorkspaceFilter( + npmPackageJsonOptions.isIgnoreAllWorkspaces(), + npmPackageJsonOptions.getExcludedWorkspaceNames(), + npmPackageJsonOptions.getIncludedWorkspaceNames()); + PackageJsonExtractor packageJsonExtractor = new PackageJsonExtractor(gson, externalIdFactory, npmPackageJsonOptions.getNpmDependencyTypeFilter(), workspaceFilter); return new NpmPackageJsonParseDetectable(environment, fileFinder, packageJsonExtractor); } diff --git a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/PackageJsonExtractorTest.java b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/PackageJsonExtractorTest.java index 6d396827a7..b7903ed788 100644 --- a/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/PackageJsonExtractorTest.java +++ b/detectable/src/test/java/com/blackduck/integration/detectable/detectables/npm/packagejson/unit/PackageJsonExtractorTest.java @@ -1,6 +1,13 @@ package com.blackduck.integration.detectable.detectables.npm.packagejson.unit; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.File; +import java.io.IOException; +import java.util.Arrays; +import java.util.Collections; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -9,6 +16,7 @@ import com.google.gson.GsonBuilder; import com.blackduck.integration.bdio.graph.DependencyGraph; import com.blackduck.integration.bdio.model.Forge; +import com.blackduck.integration.bdio.model.dependency.Dependency; import com.blackduck.integration.bdio.model.externalid.ExternalId; import com.blackduck.integration.bdio.model.externalid.ExternalIdFactory; import com.blackduck.integration.detectable.annotations.UnitTest; @@ -18,7 +26,9 @@ import com.blackduck.integration.detectable.detectables.npm.packagejson.CombinedPackageJson; import com.blackduck.integration.detectable.detectables.npm.packagejson.PackageJsonExtractor; import com.blackduck.integration.detectable.extraction.Extraction; +import com.blackduck.integration.detectable.util.FunctionalTestFiles; import com.blackduck.integration.detectable.util.graph.GraphAssert; +import com.blackduck.integration.util.ExcludedIncludedWildcardFilter; @UnitTest class PackageJsonExtractorTest { @@ -36,12 +46,79 @@ void setUp() { testDevDep2 = externalIdFactory.createNameVersionExternalId(Forge.NPMJS, "nameDev2", "versionDev2"); } + // Workspace filtering tests use the workspace-filter-test fixture: + // root package.json: { "dependencies": { "express": "4.18.0" }, "workspaces": ["packages/api", "packages/ui"] } + // packages/api/package.json: { "dependencies": { "express": "4.18.0" } } + // packages/ui/package.json: { "dependencies": { "lodash": "4.17.21" } } + + @Test + void excludedWorkspaceDepDoesNotAppearInOutput() throws IOException { + ExcludedIncludedWildcardFilter filter = ExcludedIncludedWildcardFilter.fromCollections( + Arrays.asList("packages/ui"), Collections.emptyList()); + PackageJsonExtractor extractor = createExtractorWithFilter(filter); + DependencyGraph graph = extractFromFixture(extractor); + + ExternalIdFactory factory = new ExternalIdFactory(); + ExternalId expressId = factory.createNameVersionExternalId(Forge.NPMJS, "express", "4.18.0"); + ExternalId lodashId = factory.createNameVersionExternalId(Forge.NPMJS, "lodash", "4.17.21"); + + assertTrue(graph.getRootDependencies().stream().map(Dependency::getExternalId).anyMatch(expressId::equals), + "express should appear (declared in root package.json)"); + assertFalse(graph.getRootDependencies().stream().map(Dependency::getExternalId).anyMatch(lodashId::equals), + "lodash should NOT appear when packages/ui is excluded"); + } + + @Test + void includedWorkspaceOnlyMergesThatWorkspacesDeps() throws IOException { + ExcludedIncludedWildcardFilter filter = ExcludedIncludedWildcardFilter.fromCollections( + Collections.emptyList(), Arrays.asList("packages/api")); + PackageJsonExtractor extractor = createExtractorWithFilter(filter); + DependencyGraph graph = extractFromFixture(extractor); + + ExternalIdFactory factory = new ExternalIdFactory(); + ExternalId expressId = factory.createNameVersionExternalId(Forge.NPMJS, "express", "4.18.0"); + ExternalId lodashId = factory.createNameVersionExternalId(Forge.NPMJS, "lodash", "4.17.21"); + + assertTrue(graph.getRootDependencies().stream().map(Dependency::getExternalId).anyMatch(expressId::equals), + "express should appear (declared in root package.json and packages/api)"); + assertFalse(graph.getRootDependencies().stream().map(Dependency::getExternalId).anyMatch(lodashId::equals), + "lodash should NOT appear when only packages/api is included"); + } + + @Test + void ignoreAllWorkspacesExcludesAllWorkspaceDeps() throws IOException { + ExcludedIncludedWildcardFilter filter = ExcludedIncludedWildcardFilter.fromCollections( + Collections.singletonList("*"), Collections.emptyList()); + PackageJsonExtractor extractor = createExtractorWithFilter(filter); + DependencyGraph graph = extractFromFixture(extractor); + + ExternalIdFactory factory = new ExternalIdFactory(); + ExternalId expressId = factory.createNameVersionExternalId(Forge.NPMJS, "express", "4.18.0"); + ExternalId lodashId = factory.createNameVersionExternalId(Forge.NPMJS, "lodash", "4.17.21"); + + assertTrue(graph.getRootDependencies().stream().map(Dependency::getExternalId).anyMatch(expressId::equals), + "express should appear (declared in root package.json)"); + assertFalse(graph.getRootDependencies().stream().map(Dependency::getExternalId).anyMatch(lodashId::equals), + "lodash should NOT appear when all workspaces are ignored"); + } + + private DependencyGraph extractFromFixture(PackageJsonExtractor extractor) throws IOException { + File packageJsonFile = FunctionalTestFiles.asFile("/npm/workspace-filter-test/package.json"); + Extraction extraction = extractor.extract(packageJsonFile); + return extraction.getCodeLocations().get(0).getDependencyGraph(); + } + private PackageJsonExtractor createExtractor(NpmDependencyType... excludedTypes) { Gson gson = new GsonBuilder().setPrettyPrinting().create(); EnumListFilter npmDependencyTypeFilter = EnumListFilter.fromExcluded(excludedTypes); return new PackageJsonExtractor(gson, new ExternalIdFactory(), npmDependencyTypeFilter); } + private PackageJsonExtractor createExtractorWithFilter(ExcludedIncludedWildcardFilter workspaceFilter) { + Gson gson = new GsonBuilder().setPrettyPrinting().create(); + return new PackageJsonExtractor(gson, new ExternalIdFactory(), EnumListFilter.excludeNone(), workspaceFilter); + } + @Test void extractWithNoDevOrPeerDependencies() { CombinedPackageJson packageJson = createPackageJson(); diff --git a/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java b/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java index 511039098d..4009939e6e 100644 --- a/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java +++ b/src/main/java/com/blackduck/integration/detect/configuration/DetectableOptionFactory.java @@ -262,7 +262,10 @@ public NpmLockfileOptions createNpmLockfileOptions() { public NpmPackageJsonParseDetectableOptions createNpmPackageJsonParseDetectableOptions() { EnumListFilter npmDependencyTypeFilter = createNpmDependencyTypeFilter(); - return new NpmPackageJsonParseDetectableOptions(npmDependencyTypeFilter); + List excludedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_EXCLUDED_WORKSPACES); + List includedWorkspaceNames = detectConfiguration.getValue(DetectProperties.DETECT_NPM_INCLUDED_WORKSPACES); + boolean ignoreAllWorkspaces = detectConfiguration.getValue(DetectProperties.DETECT_NPM_IGNORE_ALL_WORKSPACES_MODE); + return new NpmPackageJsonParseDetectableOptions(npmDependencyTypeFilter, excludedWorkspaceNames, includedWorkspaceNames, ignoreAllWorkspaces); } private EnumListFilter createNpmDependencyTypeFilter() {