-
Notifications
You must be signed in to change notification settings - Fork 0
feat: Added npm-packages #33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds a new npm-packages devcontainer feature that allows users to install npm packages globally. The feature is designed to work after node.js is installed and supports installing multiple packages with optional version specifications in a comma-separated format.
Changes:
- Added new npm-packages feature with installer logic and configuration
- Extended installer package with InstallGlobalPackage function for npm
- Integrated npm-packages into build system, CI workflow, and documentation
Reviewed changes
Copilot reviewed 11 out of 11 changed files in this pull request and generated 8 comments.
Show a summary per file
| File | Description |
|---|---|
| installer/npm.go | Added InstallGlobalPackage function to install npm packages globally |
| features/src/npm-packages/installer.go | Main installer logic for parsing and installing comma-separated npm packages |
| features/src/npm-packages/install.sh | Shell script entry point that invokes the Go installer |
| features/src/npm-packages/devcontainer-feature.json | Feature metadata and configuration with installsAfter dependency on node |
| features/src/npm-packages/README.md | Documentation for the npm-packages feature |
| features/test/npm-packages/scenarios.json | Test scenario configuration |
| features/test/npm-packages/test-images.json | Test image definitions (Debian 11, 12, Ubuntu 24.04) |
| features/test/npm-packages/install.sh | Test verification script |
| build/build.go | Added build, test, and publish tasks for npm-packages |
| README.md | Added npm-packages to the feature list |
| .github/workflows/ci.yml | Added npm-packages to the CI matrix |
installer/npm.go
Outdated
| func (npm) InstallGlobalPackage(npmPackage, version string) error { | ||
| packageWithVersion := npmPackage | ||
| if version != "" && version != "latest" { | ||
| packageWithVersion = fmt.Sprintf("%s@%s", npmPackage, version) | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The InstallGlobalPackage function should validate that npmPackage doesn't already contain a version specifier when version is non-empty, or document that npmPackage can be a combined "package@version" string and version parameter should be empty in that case. The current implementation allows invalid combinations like npmPackage="which@3.0.1" and version="4.0.0" which would result in attempting to install "which@3.0.1@4.0.0".
| parts := strings.Split(*version, ",") | ||
| var pkgs []string | ||
| for _, p := range parts { | ||
| trimmed := strings.TrimSpace(p) | ||
| if trimmed != "" { | ||
| pkgs = append(pkgs, trimmed) | ||
| } | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing handling for empty package list. When no packages are specified, the code should print a message like "No packages specified for installation." and return early, consistent with the system-packages feature (features/src/system-packages/installer.go:25-28).
| flag.Parse() | ||
|
|
||
| if _, err := exec.LookPath("npm"); err != nil { | ||
| return fmt.Errorf("Could not find npm, make sure to install it first.") |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Error message should start with a lowercase letter to be consistent with error message conventions throughout the codebase. All error messages in the installer package start with lowercase (e.g., "failed to install dependencies" in installer/apk.go:15, "failed getting latest version" in installer/npm.go:15). Change "Could not find" to "could not find".
| [[ -f "$(dirname "$0")/functions.sh" ]] && source "$(dirname "$0")/functions.sh" | ||
|
|
||
| check_version "$(npm list -g @devcontainers/cli 2>&1)" "@devcontainers/cli" | ||
| check_version "$(npm list -g which@3.0.1 2>&1)" "which@3.0.1" |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test checks for "which@3.0.1" in the npm list output, but npm list may show the package name and version differently depending on the npm version. Consider checking for just "which" to make the test more robust, or verify that npm list consistently outputs the package@version format across all test images.
| check_version "$(npm list -g which@3.0.1 2>&1)" "which@3.0.1" | |
| check_version "$(npm list -g which 2>&1)" "which" |
| "@devcontainers/cli,which@3.0.1" | ||
| ], | ||
| "default": "", | ||
| "description": "The packages to install, comma separated" |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The description should use "Comma-separated" (with hyphen and capital C) to be consistent with the system-packages feature (features/src/system-packages/devcontainer-feature.json:9). Change "comma separated" to "Comma-separated".
| version := flag.String("packages", "", "") | ||
| flag.Parse() | ||
|
|
||
| if _, err := exec.LookPath("npm"); err != nil { | ||
| return fmt.Errorf("Could not find npm, make sure to install it first.") | ||
| } | ||
|
|
||
| parts := strings.Split(*version, ",") |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The flag variable is named "version" but should be named "packages" to reflect what it actually contains. This is inconsistent with the flag name "-packages" and confusing for maintainability.
| version := flag.String("packages", "", "") | |
| flag.Parse() | |
| if _, err := exec.LookPath("npm"); err != nil { | |
| return fmt.Errorf("Could not find npm, make sure to install it first.") | |
| } | |
| parts := strings.Split(*version, ",") | |
| packages := flag.String("packages", "", "") | |
| flag.Parse() | |
| if _, err := exec.LookPath("npm"); err != nil { | |
| return fmt.Errorf("Could not find npm, make sure to install it first.") | |
| } | |
| parts := strings.Split(*packages, ",") |
| } | ||
| } | ||
| for _, pkg := range pkgs { | ||
| fmt.Printf("Installing '%s'", pkg) |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Missing newline character at the end of the printf statement. This will cause subsequent output to appear on the same line, making logs harder to read. Add "\n" at the end of the format string.
| fmt.Printf("Installing '%s'", pkg) | |
| fmt.Printf("Installing '%s'\n", pkg) |
| for _, pkg := range pkgs { | ||
| fmt.Printf("Installing '%s'", pkg) | ||
| if err := installer.Tools.Npm.InstallGlobalPackage(pkg, ""); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The InstallGlobalPackage function expects npmPackage and version as separate parameters, but this code passes packages that may already contain version specifiers (e.g., "which@3.0.1"). While passing an empty string for version makes this work, the API usage is confusing and error-prone. If someone were to accidentally pass a non-empty version parameter when the package string already contains a version, it would result in an invalid package specifier like "which@3.0.1@4.0.0". Consider either: (1) parsing the package string to extract name and version separately before calling InstallGlobalPackage, or (2) modifying InstallGlobalPackage to accept a single combined package@version string.
| for _, pkg := range pkgs { | |
| fmt.Printf("Installing '%s'", pkg) | |
| if err := installer.Tools.Npm.InstallGlobalPackage(pkg, ""); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| for _, pkgSpec := range pkgs { | |
| name, ver := parseNpmPackageSpec(pkgSpec) | |
| if name == "" { | |
| continue | |
| } | |
| fmt.Printf("Installing '%s'", pkgSpec) | |
| if err := installer.Tools.Npm.InstallGlobalPackage(name, ver); err != nil { | |
| return err | |
| } | |
| } | |
| return nil | |
| } | |
| // parseNpmPackageSpec splits an npm package specifier into name and version. | |
| // Examples: | |
| // "which" -> ("which", "") | |
| // "which@3.0.1" -> ("which", "3.0.1") | |
| // "@scope/pkg" -> ("@scope/pkg", "") | |
| // "@scope/pkg@3.0.1" -> ("@scope/pkg", "3.0.1") | |
| func parseNpmPackageSpec(spec string) (string, string) { | |
| spec = strings.TrimSpace(spec) | |
| if spec == "" { | |
| return "", "" | |
| } | |
| // Find the last '@'. For scoped packages without version (e.g. "@scope/pkg"), | |
| // the only '@' is at index 0, which we treat as "no version". | |
| idx := strings.LastIndex(spec, "@") | |
| if idx <= 0 { | |
| return spec, "" | |
| } | |
| name := spec[:idx] | |
| version := strings.TrimSpace(spec[idx+1:]) | |
| if version == "" { | |
| return name, "" | |
| } | |
| return name, version | |
| } |
4682d2c to
58badc2
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.
| ```json | ||
| "features": { | ||
| "ghcr.io/postfinance/devcontainer-features/npm-packages:0.1.0": { | ||
| "packages": "" |
Copilot
AI
Jan 23, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The example usage shows an empty string for the packages option, which would result in no packages being installed. Consider using a more meaningful example, such as the one from the proposals field: "@devcontainers/cli,which@3.0.1". This would better demonstrate the feature's functionality to users.
| "packages": "" | |
| "packages": "@devcontainers/cli,which@3.0.1" |
No description provided.