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
5 changes: 5 additions & 0 deletions .changeset/fix-install-node-modules-subcommand.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@shopify/cli-kit': patch
---

Fix `installNodeModules()` using the wrong subcommand for yarn, pnpm, and bun when adding specific packages. The function now accepts an optional `packages: string[]` parameter; when provided, yarn, pnpm, and bun use their `add` subcommand (required for adding new packages) while npm continues to use `install`. Existing call sites that pass only `args` are unaffected.
103 changes: 103 additions & 0 deletions packages/cli-kit/src/public/node/node-package-manager.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,109 @@ describe('install', () => {
cwd: directory,
})
})

test('uses `install` when packages are provided for npm', async () => {
// Given
const directory = '/path/to/project'

// When
await installNodeModules({
directory,
packageManager: 'npm',
packages: ['@shopify/hydrogen@2025.7.1'],
})

// Then
expect(mockedExec).toHaveBeenCalledWith('npm', ['install', '@shopify/hydrogen@2025.7.1'], {
cwd: directory,
})
})

test('uses `add` when packages are provided for yarn', async () => {
// Given
const directory = '/path/to/project'

// When
await installNodeModules({
directory,
packageManager: 'yarn',
packages: ['@shopify/hydrogen@2025.7.1'],
})

// Then
expect(mockedExec).toHaveBeenCalledWith('yarn', ['add', '@shopify/hydrogen@2025.7.1'], {
cwd: directory,
})
})

test('uses `add` when packages are provided for pnpm', async () => {
// Given
const directory = '/path/to/project'

// When
await installNodeModules({
directory,
packageManager: 'pnpm',
packages: ['@shopify/hydrogen@2025.7.1'],
})

// Then
expect(mockedExec).toHaveBeenCalledWith('pnpm', ['add', '@shopify/hydrogen@2025.7.1'], {
cwd: directory,
})
})

test('uses `add` when packages are provided for bun', async () => {
// Given
const directory = '/path/to/project'

// When
await installNodeModules({
directory,
packageManager: 'bun',
packages: ['@shopify/hydrogen@2025.7.1'],
})

// Then
expect(mockedExec).toHaveBeenCalledWith('bun', ['add', '@shopify/hydrogen@2025.7.1'], {
cwd: directory,
})
})

test('appends `args` after `packages` so flags follow the package specifiers', async () => {
// Given
const directory = '/path/to/project'

// When
await installNodeModules({
directory,
packageManager: 'yarn',
packages: ['@shopify/hydrogen@2025.7.1'],
args: ['--exact'],
})

// Then
expect(mockedExec).toHaveBeenCalledWith('yarn', ['add', '@shopify/hydrogen@2025.7.1', '--exact'], {
cwd: directory,
})
})

test('uses `install` (unchanged) when no packages are provided, even for yarn', async () => {
// Given
const directory = '/path/to/project'

// When
await installNodeModules({
directory,
packageManager: 'yarn',
args: ['--network-concurrency', '1'],
})

// Then
expect(mockedExec).toHaveBeenCalledWith('yarn', ['install', '--network-concurrency', '1'], {
cwd: directory,
})
})
})

describe('getPackageName', () => {
Expand Down
24 changes: 23 additions & 1 deletion packages/cli-kit/src/public/node/node-package-manager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,20 @@ export async function installNPMDependenciesRecursively(

interface InstallNodeModulesOptions {
directory: string
/**
* Additional flags passed to the package manager after the subcommand and
* any `packages`. Use this for things like `--network-concurrency 1`.
*/
args?: string[]
/**
* Specific package specifiers (e.g. `['@shopify/hydrogen@2025.7.1']`) to
* add to the project. When provided, `yarn`, `pnpm`, and `bun` use their
* `add` subcommand instead of `install`, since those package managers'
* `install` subcommand installs from the lockfile rather than adding new
* packages. When omitted, the function runs `<pm> install` to install
* dependencies from the lockfile (the existing behaviour).
*/
packages?: string[]
packageManager: PackageManager
stdout?: Writable
stderr?: Writable
Expand All @@ -257,7 +270,16 @@ export async function installNodeModules(options: InstallNodeModulesOptions): Pr
stderr: options.stderr,
signal: options.signal,
}
let args = ['install']
const hasPackages = Boolean(options.packages?.length)
const usesAddSubcommand =
hasPackages &&
(options.packageManager === 'yarn' ||
options.packageManager === 'pnpm' ||
options.packageManager === 'bun')
let args = [usesAddSubcommand ? 'add' : 'install']
if (options.packages) {
args = args.concat(options.packages)
}
if (options.args) {
args = args.concat(options.args)
}
Expand Down
Loading