Skip to content

Commit 8634418

Browse files
authored
#31: Implement tool commandlet for node package manager (#244)
1 parent e122f21 commit 8634418

File tree

46 files changed

+210
-103
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+210
-103
lines changed

cli/src/main/java/com/devonfw/tools/ide/commandlet/CommandletManagerImpl.java

+2
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import com.devonfw.tools.ide.tool.kotlinc.KotlincNative;
2525
import com.devonfw.tools.ide.tool.mvn.Mvn;
2626
import com.devonfw.tools.ide.tool.node.Node;
27+
import com.devonfw.tools.ide.tool.npm.Npm;
2728
import com.devonfw.tools.ide.tool.oc.Oc;
2829
import com.devonfw.tools.ide.tool.quarkus.Quarkus;
2930
import com.devonfw.tools.ide.tool.sonar.Sonar;
@@ -72,6 +73,7 @@ public CommandletManagerImpl(IdeContext context) {
7273
add(new Helm(context));
7374
add(new Java(context));
7475
add(new Node(context));
76+
add(new Npm(context));
7577
add(new Mvn(context));
7678
add(new GcViewer(context));
7779
add(new Gradle(context));

cli/src/main/java/com/devonfw/tools/ide/io/FileAccess.java

+16-16
Original file line numberDiff line numberDiff line change
@@ -15,29 +15,29 @@ public interface FileAccess {
1515
*
1616
* @param url the location of the binary file to download. May also be a local or remote path to copy from.
1717
* @param targetFile the {@link Path} to the target file to download to. Should not already exists. Missing parent
18-
* directories will be created automatically.
18+
* directories will be created automatically.
1919
*/
2020
void download(String url, Path targetFile);
2121

2222
/**
2323
* Creates the entire {@link Path} as directories if not already existing.
2424
*
2525
* @param directory the {@link Path} to
26-
* {@link java.nio.file.Files#createDirectories(Path, java.nio.file.attribute.FileAttribute...) create}.
26+
* {@link java.nio.file.Files#createDirectories(Path, java.nio.file.attribute.FileAttribute...) create}.
2727
*/
2828
void mkdirs(Path directory);
2929

3030
/**
3131
* @param file the {@link Path} to check.
3232
* @return {@code true} if the given {@code file} points to an existing file, {@code false} otherwise (the given
33-
* {@link Path} does not exist or is a directory).
33+
* {@link Path} does not exist or is a directory).
3434
*/
3535
boolean isFile(Path file);
3636

3737
/**
3838
* @param folder the {@link Path} to check.
3939
* @return {@code true} if the given {@code folder} points to an existing directory, {@code false} otherwise (a
40-
* warning is logged in this case).
40+
* warning is logged in this case).
4141
*/
4242
boolean isExpectedFolder(Path folder);
4343

@@ -87,7 +87,7 @@ default void symlink(Path source, Path targetLink) {
8787
/**
8888
* @param source the source {@link Path file or folder} to copy.
8989
* @param target the {@link Path} to copy {@code source} to. See {@link #copy(Path, Path, FileCopyMode)} for details.
90-
* will always ensure that in the end you will find the same content of {@code source} in {@code target}.
90+
* will always ensure that in the end you will find the same content of {@code source} in {@code target}.
9191
*/
9292
default void copy(Path source, Path target) {
9393

@@ -97,13 +97,13 @@ default void copy(Path source, Path target) {
9797
/**
9898
* @param source the source {@link Path file or folder} to copy.
9999
* @param target the {@link Path} to copy {@code source} to. Unlike the Linux {@code cp} command this method will not
100-
* take the filename of {@code source} and copy that to {@code target} in case that is an existing folder. Instead it
101-
* will always be simple and stupid and just copy from {@code source} to {@code target}. Therefore the result is
102-
* always clear and easy to predict and understand. Also you can easily rename a file to copy. While
103-
* {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method will always
104-
* ensure that in the end you will find the same content of {@code source} in {@code target}.
100+
* take the filename of {@code source} and copy that to {@code target} in case that is an existing folder.
101+
* Instead it will always be simple and stupid and just copy from {@code source} to {@code target}. Therefore
102+
* the result is always clear and easy to predict and understand. Also you can easily rename a file to copy.
103+
* While {@code cp my-file target} may lead to a different result than {@code cp my-file target/} this method
104+
* will always ensure that in the end you will find the same content of {@code source} in {@code target}.
105105
* @param fileOnly - {@code true} if {@code fileOrFolder} is expected to be a file and an exception shall be thrown if
106-
* it is a directory, {@code false} otherwise (copy recursively).
106+
* it is a directory, {@code false} otherwise (copy recursively).
107107
*/
108108
void copy(Path source, Path target, FileCopyMode fileOnly);
109109

@@ -120,7 +120,7 @@ default void extract(Path archiveFile, Path targetDir) {
120120
* @param archiveFile the {@link Path} to the archive file to extract.
121121
* @param targetDir the {@link Path} to the directory where to extract the {@code archiveFile}.
122122
* @param postExtractHook the {@link Consumer} to be called after the extraction on the final folder before it is
123-
* moved to {@code targetDir}.
123+
* moved to {@code targetDir}.
124124
*/
125125
default void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook) {
126126

@@ -131,7 +131,7 @@ default void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtrac
131131
* @param archiveFile the {@link Path} to the archive file to extract.
132132
* @param targetDir the {@link Path} to the directory where to extract the {@code archiveFile}.
133133
* @param postExtractHook the {@link Consumer} to be called after the extraction on the final folder before it is
134-
* moved to {@code targetDir}.
134+
* moved to {@code targetDir}.
135135
* @param extract {@code true} if the {@code archiveFile} should be extracted (default), {@code false} otherwise.
136136
*/
137137
void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract);
@@ -201,7 +201,7 @@ default void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtrac
201201
* {@link #delete(Path) delete} it after the work is done.
202202
*
203203
* @param name the default name of the temporary directory to create. A prefix or suffix may be added to ensure
204-
* uniqueness.
204+
* uniqueness.
205205
* @return the {@link Path} to the newly created and unique temporary directory.
206206
*/
207207
Path createTempDir(String name);
@@ -217,9 +217,9 @@ default void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtrac
217217
/**
218218
* @param dir the {@link Path} to the directory where to list the children.
219219
* @param filter the {@link Predicate} used to {@link Predicate#test(Object) decide} which children to include (if
220-
* {@code true} is returned).
220+
* {@code true} is returned).
221221
* @return all children of the given {@link Path} that match the given {@link Predicate}. Will be the empty list of
222-
* the given {@link Path} is not an existing directory.
222+
* the given {@link Path} is not an existing directory.
223223
*/
224224
List<Path> listChildren(Path dir, Predicate<Path> filter);
225225

cli/src/main/java/com/devonfw/tools/ide/io/FileAccessImpl.java

+41-34
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,5 @@
11
package com.devonfw.tools.ide.io;
22

3-
import com.devonfw.tools.ide.cli.CliException;
4-
import com.devonfw.tools.ide.context.IdeContext;
5-
import com.devonfw.tools.ide.os.SystemInfoImpl;
6-
import com.devonfw.tools.ide.process.ProcessContext;
7-
import com.devonfw.tools.ide.url.model.file.UrlChecksum;
8-
import com.devonfw.tools.ide.util.DateTimeUtil;
9-
import com.devonfw.tools.ide.util.FilenameUtil;
10-
import com.devonfw.tools.ide.util.HexUtil;
11-
12-
import org.apache.commons.compress.archivers.ArchiveEntry;
13-
import org.apache.commons.compress.archivers.ArchiveInputStream;
14-
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
15-
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
16-
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
17-
183
import java.io.BufferedOutputStream;
194
import java.io.FileInputStream;
205
import java.io.FileOutputStream;
@@ -47,6 +32,21 @@
4732
import java.util.function.Predicate;
4833
import java.util.stream.Stream;
4934

35+
import org.apache.commons.compress.archivers.ArchiveEntry;
36+
import org.apache.commons.compress.archivers.ArchiveInputStream;
37+
import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
38+
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
39+
import org.apache.commons.compress.archivers.zip.ZipArchiveInputStream;
40+
41+
import com.devonfw.tools.ide.cli.CliException;
42+
import com.devonfw.tools.ide.context.IdeContext;
43+
import com.devonfw.tools.ide.os.SystemInfoImpl;
44+
import com.devonfw.tools.ide.process.ProcessContext;
45+
import com.devonfw.tools.ide.url.model.file.UrlChecksum;
46+
import com.devonfw.tools.ide.util.DateTimeUtil;
47+
import com.devonfw.tools.ide.util.FilenameUtil;
48+
import com.devonfw.tools.ide.util.HexUtil;
49+
5050
/**
5151
* Implementation of {@link FileAccess}.
5252
*/
@@ -280,10 +280,12 @@ public void copy(Path source, Path target, FileCopyMode mode) {
280280
if (fileOnly) {
281281
this.context.debug("Copying file {} to {}", source, target);
282282
if (Files.isDirectory(target)) {
283-
// if we want to copy "file.txt" to the existing folder "path/to/folder/" in a shell this will copy "file.txt" into that folder
283+
// if we want to copy "file.txt" to the existing folder "path/to/folder/" in a shell this will copy "file.txt"
284+
// into that folder
284285
// with Java NIO the raw copy method will fail as we cannot copy the file to the path of the target folder
285286
// even worse if FileCopyMode is override the target folder ("path/to/folder/") would be deleted and the result
286-
// of our "file.txt" would later appear in "path/to/folder". To prevent such bugs we append the filename to target
287+
// of our "file.txt" would later appear in "path/to/folder". To prevent such bugs we append the filename to
288+
// target
287289
target = target.resolve(source.getFileName());
288290
}
289291
} else {
@@ -355,7 +357,7 @@ private void deleteLinkIfExists(Path path) throws IOException {
355357
*
356358
* @param source the {@link Path} to adapt.
357359
* @param targetLink the {@link Path} used to calculate the relative path to the {@code source} if {@code relative} is
358-
* set to {@code true}.
360+
* set to {@code true}.
359361
* @param relative the {@code relative} flag.
360362
* @return the adapted {@link Path}.
361363
* @see FileAccessImpl#symlink(Path, Path, boolean)
@@ -413,7 +415,8 @@ private void createWindowsJunction(Path source, Path targetLink) {
413415
} catch (IOException e) {
414416
throw new IllegalStateException(
415417
"Since Windows junctions are used, the source must be an absolute path. The transformation of the passed "
416-
+ "source (" + source + ") to an absolute path failed.", e);
418+
+ "source (" + source + ") to an absolute path failed.",
419+
e);
417420
}
418421

419422
} else {
@@ -435,9 +438,8 @@ public void symlink(Path source, Path targetLink, boolean relative) {
435438
try {
436439
adaptedSource = adaptPath(source, targetLink, relative);
437440
} catch (IOException e) {
438-
throw new IllegalStateException(
439-
"Failed to adapt source for source (" + source + ") target (" + targetLink + ") and relative (" + relative
440-
+ ")", e);
441+
throw new IllegalStateException("Failed to adapt source for source (" + source + ") target (" + targetLink
442+
+ ") and relative (" + relative + ")", e);
441443
}
442444
this.context.trace("Creating {} symbolic link {} pointing to {}", adaptedSource.isAbsolute() ? "" : "relative",
443445
targetLink, adaptedSource);
@@ -451,7 +453,7 @@ public void symlink(Path source, Path targetLink, boolean relative) {
451453
try {
452454
Files.createSymbolicLink(targetLink, adaptedSource);
453455
} catch (FileSystemException e) {
454-
if (this.context.getSystemInfo().isWindows()) {
456+
if (SystemInfoImpl.INSTANCE.isWindows()) {
455457
this.context.info("Due to lack of permissions, Microsoft's mklink with junction had to be used to create "
456458
+ "a Symlink. See https://github.com/devonfw/IDEasy/blob/main/documentation/symlinks.asciidoc for "
457459
+ "further details. Error was: " + e.getMessage());
@@ -460,9 +462,8 @@ public void symlink(Path source, Path targetLink, boolean relative) {
460462
throw new RuntimeException(e);
461463
}
462464
} catch (IOException e) {
463-
throw new IllegalStateException(
464-
"Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative") + "symbolic link " + targetLink
465-
+ " pointing to " + source, e);
465+
throw new IllegalStateException("Failed to create a " + (adaptedSource.isAbsolute() ? "" : "relative")
466+
+ "symbolic link " + targetLink + " pointing to " + source, e);
466467
}
467468
}
468469

@@ -505,13 +506,14 @@ public Path createTempDir(String name) {
505506
public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtractHook, boolean extract) {
506507

507508
if (Files.isDirectory(archiveFile)) {
508-
Path properInstallDir = archiveFile; //getProperInstallationSubDirOf(archiveFile, archiveFile);
509+
Path properInstallDir = archiveFile; // getProperInstallationSubDirOf(archiveFile, archiveFile);
509510
if (extract) {
510511
this.context.warning("Found directory for download at {} hence copying without extraction!", archiveFile);
511512
copy(properInstallDir, targetDir, FileCopyMode.COPY_TREE_OVERRIDE_TREE);
512513
} else {
513514
move(properInstallDir, targetDir);
514515
}
516+
postExtractHook(postExtractHook, properInstallDir);
515517
return;
516518
} else if (!extract) {
517519
mkdirs(targetDir);
@@ -551,26 +553,31 @@ public void extract(Path archiveFile, Path targetDir, Consumer<Path> postExtract
551553
}
552554
}
553555
Path properInstallDir = getProperInstallationSubDirOf(tmpDir, archiveFile);
556+
postExtractHook(postExtractHook, properInstallDir);
557+
move(properInstallDir, targetDir);
558+
delete(tmpDir);
559+
}
560+
561+
private void postExtractHook(Consumer<Path> postExtractHook, Path properInstallDir) {
562+
554563
if (postExtractHook != null) {
555564
postExtractHook.accept(properInstallDir);
556565
}
557-
move(properInstallDir, targetDir);
558-
delete(tmpDir);
559566
}
560567

561568
/**
562569
* @param path the {@link Path} to start the recursive search from.
563570
* @return the deepest subdir {@code s} of the passed path such that all directories between {@code s} and the passed
564-
* path (including {@code s}) are the sole item in their respective directory and {@code s} is not named "bin".
571+
* path (including {@code s}) are the sole item in their respective directory and {@code s} is not named
572+
* "bin".
565573
*/
566574
private Path getProperInstallationSubDirOf(Path path, Path archiveFile) {
567575

568576
try (Stream<Path> stream = Files.list(path)) {
569577
Path[] subFiles = stream.toArray(Path[]::new);
570578
if (subFiles.length == 0) {
571-
throw new CliException(
572-
"The downloaded package " + archiveFile + " seems to be empty as you can check in the extracted folder "
573-
+ path);
579+
throw new CliException("The downloaded package " + archiveFile
580+
+ " seems to be empty as you can check in the extracted folder " + path);
574581
} else if (subFiles.length == 1) {
575582
String filename = subFiles[0].getFileName().toString();
576583
if (!filename.equals(IdeContext.FOLDER_BIN) && !filename.equals(IdeContext.FOLDER_CONTENTS)
@@ -598,7 +605,7 @@ public void extractTar(Path file, Path targetDir, TarCompression compression) {
598605

599606
/**
600607
* @param permissions The integer as returned by {@link TarArchiveEntry#getMode()} that represents the file
601-
* permissions of a file on a Unix file system.
608+
* permissions of a file on a Unix file system.
602609
* @return A String representing the file permissions. E.g. "rwxrwxr-x" or "rw-rw-r--"
603610
*/
604611
public static String generatePermissionString(int permissions) {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.devonfw.tools.ide.tool.npm;
2+
3+
import java.nio.file.Path;
4+
import java.util.Set;
5+
6+
import com.devonfw.tools.ide.common.Tag;
7+
import com.devonfw.tools.ide.context.IdeContext;
8+
import com.devonfw.tools.ide.io.FileAccess;
9+
import com.devonfw.tools.ide.tool.LocalToolCommandlet;
10+
import com.devonfw.tools.ide.tool.ToolCommandlet;
11+
import com.devonfw.tools.ide.tool.node.Node;
12+
13+
/**
14+
* {@link ToolCommandlet} for <a href="https://www.npmjs.com/">npm</a>.
15+
*/
16+
public class Npm extends LocalToolCommandlet {
17+
18+
/**
19+
* The constructor.
20+
*
21+
* @param context the {@link IdeContext}.
22+
*/
23+
public Npm(IdeContext context) {
24+
25+
super(context, "npm", Set.of(Tag.JAVA_SCRIPT, Tag.BUILD));
26+
}
27+
28+
@Override
29+
public boolean install(boolean silent) {
30+
31+
getCommandlet(Node.class).install();
32+
return super.doInstall(silent);
33+
}
34+
35+
protected void postExtract(Path extractedDir) {
36+
37+
FileAccess fileAccess = context.getFileAccess();
38+
if (context.getSystemInfo().isWindows()) {
39+
Path nodeHomePath = this.context.getSoftwarePath().resolve("node/");
40+
Path npmBinBath = nodeHomePath.resolve("node_modules/npm/bin/");
41+
String npm = "npm";
42+
String npx = "npx";
43+
String cmd = ".cmd";
44+
45+
fileAccess.delete(nodeHomePath.resolve(npm));
46+
fileAccess.delete(nodeHomePath.resolve(npm + cmd));
47+
fileAccess.delete(nodeHomePath.resolve(npx));
48+
fileAccess.delete(nodeHomePath.resolve(npx + cmd));
49+
50+
fileAccess.copy(npmBinBath.resolve(npm), nodeHomePath.resolve(npm));
51+
fileAccess.copy(npmBinBath.resolve(npm + cmd), nodeHomePath.resolve(npm + cmd));
52+
fileAccess.copy(npmBinBath.resolve(npx), nodeHomePath.resolve(npx));
53+
fileAccess.copy(npmBinBath.resolve(npx + cmd), nodeHomePath.resolve(npx + cmd));
54+
}
55+
}
56+
}

cli/src/main/resources/nls/Ide.properties

+1
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ cmd-list-editions=List the available editions of the selected tool.
2525
cmd-list-versions=List the available versions of the selected tool.
2626
cmd-mvn=Tool commandlet for Maven (Build-Tool)
2727
cmd-node=Tool commandlet for Node.js (JavaScript runtime)
28+
cmd-npm=Tool commandlet for Npm
2829
cmd-oc=Tool commandlet for Openshift CLI (Kubernetes Management Tool)
2930
cmd-quarkus=Tool commandlet for Quarkus (Framework for cloud-native apps)
3031
cmd-set-edition=Set the edition of the selected tool.

cli/src/main/resources/nls/Ide_de.properties

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ cmd-list-editions=Listet die verfügbaren Editionen des selektierten Werkzeugs a
2323
cmd-list-versions=Listet die verfügbaren Versionen des selektierten Werkzeugs auf.
2424
cmd-mvn=Werkzeug Kommando für Maven (Build-Werkzeug)
2525
cmd-node=Werkzeug Kommando für Node.js (JavaScript Laufzeitumgebung)
26+
cmd-npm=Werkzeug Kommando für Npm
2627
cmd-oc=Werkzeug Kommando für Openshift CLI (Kubernetes Management Tool)
2728
cmd-quarkus=Werkzeug Kommando für Quarkus (Framework für Cloud-native Anwendungen)
2829
cmd-set-edition=Setzt die Edition des selektierten Werkzeugs.

cli/src/test/java/com/devonfw/tools/ide/tool/jmc/JmcTest.java

-1
Original file line numberDiff line numberDiff line change
@@ -82,5 +82,4 @@ private void checkInstallation(IdeTestContext context) {
8282
assertThat(context.getSoftwarePath().resolve("jmc/.ide.software.version")).exists().hasContent("8.3.0");
8383
assertLogMessage(context, IdeLogLevel.SUCCESS, "Successfully installed jmc in version 8.3.0");
8484
}
85-
8685
}

0 commit comments

Comments
 (0)