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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 55 additions & 2 deletions src/main/java/com/jfrog/ide/common/npm/NpmComponentUpdater.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,18 @@
package com.jfrog.ide.common.npm;

import com.jfrog.ide.common.updateversion.ComponentUpdater;
import com.jfrog.ide.common.utils.WslUtils;
import org.apache.commons.lang3.StringUtils;
import org.jfrog.build.api.util.Log;
import org.jfrog.build.extractor.executor.CommandExecutor;
import org.jfrog.build.extractor.executor.CommandResults;
import org.jfrog.build.extractor.npm.NpmDriver;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;

/**
Expand All @@ -19,10 +25,33 @@ public class NpmComponentUpdater extends ComponentUpdater {

public static final String NPM_VERSION_DELIMITER = "@";
private final NpmDriver npmDriver;
private final CommandExecutor wslExecutor;
private final boolean isWsl;

public NpmComponentUpdater(Path projectDir, Log logger, Map<String, String> env) {
super(projectDir, logger);
this.npmDriver = new NpmDriver(env);
this.isWsl = WslUtils.isWslPath(projectDir);
if (isWsl) {
this.npmDriver = null;
this.wslExecutor = new CommandExecutor("wsl.exe", env);
} else {
this.npmDriver = new NpmDriver(env);
this.wslExecutor = null;
}
}

/**
* Prefix arguments for {@code wsl.exe} so npm runs in the project directory inside WSL
* (aligned with {@link NpmTreeBuilder}).
*/
private List<String> wslNpmInvocationPrefix() {
String linuxPath = WslUtils.toLinuxPath(projectDir.toString());
List<String> args = new ArrayList<>();
args.add("--cd");
args.add(linuxPath);
args.add("--exec");
args.add("npm");
return args;
}

/**
Expand All @@ -35,7 +64,21 @@ public NpmComponentUpdater(Path projectDir, Log logger, Map<String, String> env)
@Override
public void run(String componentName, String componentVersion) throws IOException {
super.run(componentName, componentVersion);
npmDriver.install(projectDir.toFile(), Collections.singletonList(this.componentFullName), this.logger);
if (isWsl) {
List<String> args = new ArrayList<>(wslNpmInvocationPrefix());
args.add("install");
args.add(this.componentFullName);
try {
CommandResults res = wslExecutor.exeCommand(null, args, null, logger);
if (!res.isOk()) {
throw new IOException(StringUtils.defaultString(res.getErr()) + StringUtils.defaultString(res.getRes()));
}
} catch (IOException | InterruptedException e) {
throw new IOException("npm install failed via WSL", e);
}
} else {
npmDriver.install(projectDir.toFile(), Collections.singletonList(this.componentFullName), this.logger);
}
}

@Override
Expand All @@ -45,6 +88,16 @@ public String getVersionDelimiter() {

@Override
public boolean isDriverInstalled() {
if (isWsl) {
try {
List<String> args = new ArrayList<>(wslNpmInvocationPrefix());
args.add("--version");
CommandResults results = wslExecutor.exeCommand(null, args, null, null);
return results.isOk() && !StringUtils.isBlank(results.getRes());
} catch (Exception e) {
return false;
}
}
return npmDriver.isNpmInstalled();
}

Expand Down
86 changes: 81 additions & 5 deletions src/main/java/com/jfrog/ide/common/npm/NpmTreeBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.node.ObjectNode;
import com.google.common.collect.Lists;
import com.jfrog.ide.common.deptree.DepTree;
import com.jfrog.ide.common.deptree.DepTreeNode;
import com.jfrog.ide.common.utils.Utils;
import com.jfrog.ide.common.utils.WslUtils;
import org.apache.commons.lang3.StringUtils;
import org.jfrog.build.api.util.Log;
import org.jfrog.build.extractor.executor.CommandExecutor;
import org.jfrog.build.extractor.executor.CommandResults;
import org.jfrog.build.extractor.npm.NpmDriver;

import java.io.IOException;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
Expand All @@ -21,14 +29,24 @@
*/
public class NpmTreeBuilder {
private static final ObjectMapper objectMapper = new ObjectMapper();
private static final ObjectReader jsonReader = objectMapper.reader();
private final NpmDriver npmDriver;
private final CommandExecutor wslExecutor;
private final boolean isWsl;
private final Path projectDir;
private final String descriptorFilePath;

public NpmTreeBuilder(Path projectDir, String descriptorFilePath, Map<String, String> env) {
this.projectDir = projectDir;
this.descriptorFilePath = descriptorFilePath;
this.npmDriver = new NpmDriver(env);
this.isWsl = WslUtils.isWslPath(projectDir);
if (isWsl) {
this.npmDriver = null;
this.wslExecutor = new CommandExecutor("wsl.exe", env);
} else {
this.npmDriver = new NpmDriver(env);
this.wslExecutor = null;
}
}

/**
Expand All @@ -39,15 +57,15 @@ public NpmTreeBuilder(Path projectDir, String descriptorFilePath, Map<String, St
* @throws IOException in case of I/O error.
*/
public DepTree buildTree(Log logger) throws IOException {
if (!npmDriver.isNpmInstalled()) {
throw new IOException("Could not scan npm project dependencies, because npm CLI is not in the PATH.");
if (!isNpmInstalled()) {
throw new IOException("Could not scan npm project dependencies, because npm CLI is not in the PATH. [WSL=" + this.isWsl + "]");
}
JsonNode prodResults = npmDriver.list(projectDir.toFile(), Lists.newArrayList("--prod", "--package-lock-only"));
JsonNode prodResults = npmList(Lists.newArrayList("--prod", "--package-lock-only"));
if (prodResults.get("problems") != null) {
logger.warn("Errors occurred during building the Npm dependency tree. " +
"The dependency tree may be incomplete:\n" + prodResults.get("problems").toString());
}
JsonNode devResults = npmDriver.list(projectDir.toFile(), Lists.newArrayList("--dev", "--package-lock-only"));
JsonNode devResults = npmList(Lists.newArrayList("--dev", "--package-lock-only"));
Map<String, DepTreeNode> nodes = new HashMap<>();
String packageId = getPackageId(prodResults);
addDepTreeNodes(nodes, prodResults, packageId, "prod");
Expand All @@ -57,6 +75,64 @@ public DepTree buildTree(Log logger) throws IOException {
return tree;
}

/**
* Check whether npm is available, accounting for WSL projects.
* For WSL projects, npm is invoked through {@code wsl.exe}.
*/
private boolean isNpmInstalled() {
if (isWsl) {
try {
List<String> args = wslNpmInvocationPrefix();
args.add("--version");
CommandResults results = wslExecutor.exeCommand(null, args, null, null);
return results.isOk() && !StringUtils.isBlank(results.getRes());
} catch (Exception e) {
return false;
}
}
return npmDriver.isNpmInstalled();
}

/**
* Prefix arguments for {@code wsl.exe} so npm runs in the project directory inside WSL
* (same {@code --cd} context as {@link #npmList}).
*/
private List<String> wslNpmInvocationPrefix() {
String linuxPath = WslUtils.toLinuxPath(projectDir.toString());
List<String> args = new ArrayList<>();
args.add("--cd");
args.add(linuxPath);
args.add("--exec");
args.add("npm");
return args;
}

/**
* Run {@code npm ls} and return the parsed JSON output.
* For WSL projects, the command is routed through {@code wsl.exe --cd <linux-path> --exec npm ...}.
*/
private JsonNode npmList(List<String> extraArgs) throws IOException {
if (isWsl) {
List<String> args = new ArrayList<>(wslNpmInvocationPrefix());
args.add("ls");
args.add("--json");
args.add("--all");
args.addAll(extraArgs);
try {
CommandResults commandRes = wslExecutor.exeCommand(null, args, null, null);
String res = StringUtils.isBlank(commandRes.getRes()) ? "{}" : commandRes.getRes();
JsonNode results = jsonReader.readTree(res);
if (!commandRes.isOk() && !results.has("problems") && results.isObject()) {
((ObjectNode) results).put("problems", commandRes.getErr());
}
return results;
} catch (IOException | InterruptedException e) {
throw new IOException("npm ls failed via WSL", e);
}
}
return npmDriver.list(projectDir.toFile(), extraArgs);
}

private void addDepTreeNodes(Map<String, DepTreeNode> nodes, JsonNode jsonDep, String depId, String scope) {
DepTreeNode depNode;
if (nodes.containsKey(depId)) {
Expand Down
87 changes: 87 additions & 0 deletions src/main/java/com/jfrog/ide/common/utils/WslUtils.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package com.jfrog.ide.common.utils;

import java.nio.file.Path;

/**
* Utility methods for WSL (Windows Subsystem for Linux) path handling.
* On Windows, WSL filesystems are exposed as UNC paths under the wsl.localhost or wsl$ hosts.
*/
public class WslUtils {
// UNC-style prefixes used by Windows to expose WSL filesystems
private static final String WSL_LOCALHOST_PREFIX = "\\\\wsl.localhost\\";
private static final String WSL_DOLLAR_PREFIX = "\\\\wsl$\\";

private WslUtils() {
}

private static boolean startsWithIgnoreCase(String s, String prefix) {
return s.length() >= prefix.length() && s.regionMatches(true, 0, prefix, 0, prefix.length());
}

/**
* Normalizes Windows extended-length UNC prefixes so WSL detection sees {@code \\wsl$\...} / {@code \\wsl.localhost\...}.
* Example: {@code \\?\UNC\wsl$\Ubuntu\home\...} becomes {@code \\wsl$\Ubuntu\home\...}.
*/
static String normalizePathStringForWsl(String path) {
if (path == null || path.isEmpty()) {
return path;
}
String p = path;
if (startsWithIgnoreCase(p, "\\\\?\\UNC\\")) {
return "\\\\" + p.substring("\\\\?\\UNC\\".length());
}
if (startsWithIgnoreCase(p, "\\\\?\\")) {
return p.substring("\\\\?\\".length());
}
return p;
}

/**
* Returns true if the given path string refers to a WSL filesystem
* (i.e. it is a UNC path rooted at the wsl.localhost or wsl$ host).
*/
public static boolean isWslPath(String path) {
if (path == null) {
return false;
}
String normalized = normalizePathStringForWsl(path);
return startsWithIgnoreCase(normalized, WSL_LOCALHOST_PREFIX) || startsWithIgnoreCase(normalized, WSL_DOLLAR_PREFIX);
}

/**
* Returns true if the given {@link Path} refers to a WSL filesystem.
*/
public static boolean isWslPath(Path path) {
return path != null && isWslPath(path.toString());
}

/**
* Converts a Windows-style WSL UNC path to the equivalent Linux path inside WSL.
* The distro name (first UNC component after the host) is stripped, and
* backslashes are replaced with forward slashes.
*
* @param wslWindowsPath the Windows-style WSL path
* @return the Linux path, or the original string unchanged if it is not a WSL path
*/
public static String toLinuxPath(String wslWindowsPath) {
if (wslWindowsPath == null) {
return null;
}
String p = normalizePathStringForWsl(wslWindowsPath);
String withoutPrefix;
if (startsWithIgnoreCase(p, WSL_LOCALHOST_PREFIX)) {
withoutPrefix = p.substring(WSL_LOCALHOST_PREFIX.length());
} else if (startsWithIgnoreCase(p, WSL_DOLLAR_PREFIX)) {
withoutPrefix = p.substring(WSL_DOLLAR_PREFIX.length());
} else {
return wslWindowsPath;
}
// withoutPrefix is now: <distro>\<rest-of-path>
// Strip the distro name (first path component) to obtain the Linux path.
int firstBackslash = withoutPrefix.indexOf('\\');
if (firstBackslash == -1) {
return "/"; // Path pointed at the distro root
}
return withoutPrefix.substring(firstBackslash).replace('\\', '/');
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.jfrog.ide.common.yarn;

import com.jfrog.ide.common.updateversion.ComponentUpdater;
import com.jfrog.ide.common.utils.WslUtils;
import org.jfrog.build.api.util.Log;

import java.io.IOException;
Expand All @@ -14,7 +15,7 @@ public class YarnComponentUpdater extends ComponentUpdater {

public YarnComponentUpdater(Path projectDir, Log logger, Map<String, String> env) {
super(projectDir, logger);
this.yarnDriver = new YarnDriver(env);
this.yarnDriver = new YarnDriver(env, logger, WslUtils.isWslPath(projectDir));
}

/**
Expand All @@ -37,7 +38,7 @@ public String getVersionDelimiter() {

@Override
public boolean isDriverInstalled() {
return yarnDriver.isYarnInstalled();
return yarnDriver.isYarnInstalled(projectDir.toFile());
}

@Override
Expand Down
Loading
Loading