Skip to content

Conversation

@Roemer
Copy link
Member

@Roemer Roemer commented Jan 22, 2026

No description provided.

Copy link

Copilot AI left a 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
Comment on lines 32 to 36
func (npm) InstallGlobalPackage(npmPackage, version string) error {
packageWithVersion := npmPackage
if version != "" && version != "latest" {
packageWithVersion = fmt.Sprintf("%s@%s", npmPackage, version)
}
Copy link

Copilot AI Jan 23, 2026

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".

Copilot uses AI. Check for mistakes.
Comment on lines 32 to 39
parts := strings.Split(*version, ",")
var pkgs []string
for _, p := range parts {
trimmed := strings.TrimSpace(p)
if trimmed != "" {
pkgs = append(pkgs, trimmed)
}
}
Copy link

Copilot AI Jan 23, 2026

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).

Copilot uses AI. Check for mistakes.
flag.Parse()

if _, err := exec.LookPath("npm"); err != nil {
return fmt.Errorf("Could not find npm, make sure to install it first.")
Copy link

Copilot AI Jan 23, 2026

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".

Copilot uses AI. Check for mistakes.
[[ -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"
Copy link

Copilot AI Jan 23, 2026

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.

Suggested change
check_version "$(npm list -g which@3.0.1 2>&1)" "which@3.0.1"
check_version "$(npm list -g which 2>&1)" "which"

Copilot uses AI. Check for mistakes.
"@devcontainers/cli,which@3.0.1"
],
"default": "",
"description": "The packages to install, comma separated"
Copy link

Copilot AI Jan 23, 2026

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".

Copilot uses AI. Check for mistakes.
Comment on lines 25 to 32
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, ",")
Copy link

Copilot AI Jan 23, 2026

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.

Suggested change
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, ",")

Copilot uses AI. Check for mistakes.
}
}
for _, pkg := range pkgs {
fmt.Printf("Installing '%s'", pkg)
Copy link

Copilot AI Jan 23, 2026

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.

Suggested change
fmt.Printf("Installing '%s'", pkg)
fmt.Printf("Installing '%s'\n", pkg)

Copilot uses AI. Check for mistakes.
Comment on lines 40 to 51
for _, pkg := range pkgs {
fmt.Printf("Installing '%s'", pkg)
if err := installer.Tools.Npm.InstallGlobalPackage(pkg, ""); err != nil {
return err
}
}
return nil
}
Copy link

Copilot AI Jan 23, 2026

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.

Suggested change
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
}

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI left a 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": ""
Copy link

Copilot AI Jan 23, 2026

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.

Suggested change
"packages": ""
"packages": "@devcontainers/cli,which@3.0.1"

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants