diff --git a/packages/app/src/cli/services/function/build.test.ts b/packages/app/src/cli/services/function/build.test.ts index addea2de09..9d006530ef 100644 --- a/packages/app/src/cli/services/function/build.test.ts +++ b/packages/app/src/cli/services/function/build.test.ts @@ -22,12 +22,20 @@ import { import {testApp, testFunctionExtension} from '../../models/app/app.test-data.js' import {beforeEach, describe, expect, test, vi} from 'vitest' import {exec} from '@shopify/cli-kit/node/system' +import {packageManagerBinaryCommandForDirectory} from '@shopify/cli-kit/node/node-package-manager' import {dirname, joinPath} from '@shopify/cli-kit/node/path' import {inTemporaryDirectory, mkdir, readFileSync, writeFile, removeFile} from '@shopify/cli-kit/node/fs' import {build as esBuild} from 'esbuild' vi.mock('@shopify/cli-kit/node/fs') vi.mock('@shopify/cli-kit/node/system') +vi.mock('@shopify/cli-kit/node/node-package-manager', async () => { + const actual: any = await vi.importActual('@shopify/cli-kit/node/node-package-manager') + return { + ...actual, + packageManagerBinaryCommandForDirectory: vi.fn(), + } +}) vi.mock('./binaries.js', async (importOriginal) => { const actual: any = await importOriginal() @@ -76,6 +84,10 @@ beforeEach(async () => { stderr = {write: vi.fn()} stdout = {write: vi.fn()} signal = vi.fn() + vi.mocked(packageManagerBinaryCommandForDirectory).mockResolvedValue({ + command: 'npm', + args: ['exec', '--', 'graphql-code-generator', '--config', 'package.json'], + }) }) describe('buildGraphqlTypes', () => { @@ -88,6 +100,13 @@ describe('buildGraphqlTypes', () => { // Then await expect(got).resolves.toBeUndefined() + expect(packageManagerBinaryCommandForDirectory).toHaveBeenCalledTimes(1) + expect(packageManagerBinaryCommandForDirectory).toHaveBeenCalledWith( + ourFunction.directory, + 'graphql-code-generator', + '--config', + 'package.json', + ) expect(exec).toHaveBeenCalledWith('npm', ['exec', '--', 'graphql-code-generator', '--config', 'package.json'], { cwd: ourFunction.directory, stderr, @@ -95,6 +114,26 @@ describe('buildGraphqlTypes', () => { }) }) + test('generate types executes the command returned by the shared helper', {timeout: 20000}, async () => { + // Given + const ourFunction = await testFunctionExtension({entryPath: 'src/index.js'}) + vi.mocked(packageManagerBinaryCommandForDirectory).mockResolvedValue({ + command: 'pnpm', + args: ['exec', 'graphql-code-generator', '--config', 'package.json'], + }) + + // When + const got = buildGraphqlTypes(ourFunction, {stdout, stderr, signal, app}) + + // Then + await expect(got).resolves.toBeUndefined() + expect(exec).toHaveBeenCalledWith('pnpm', ['exec', 'graphql-code-generator', '--config', 'package.json'], { + cwd: ourFunction.directory, + stderr, + signal, + }) + }) + test('errors if function is not a JS function and no typegen_command', async () => { // Given const ourFunction = await testFunctionExtension() @@ -105,6 +144,7 @@ describe('buildGraphqlTypes', () => { // Then await expect(got).rejects.toThrow(/No typegen_command specified/) + expect(packageManagerBinaryCommandForDirectory).not.toHaveBeenCalled() }) test('runs custom typegen_command when provided', async () => { @@ -129,6 +169,7 @@ describe('buildGraphqlTypes', () => { // Then await expect(got).resolves.toBeUndefined() + expect(packageManagerBinaryCommandForDirectory).not.toHaveBeenCalled() expect(exec).toHaveBeenCalledWith('npx', ['shopify-function-codegen', '--schema', 'schema.graphql'], { cwd: ourFunction.directory, stdout, @@ -159,6 +200,7 @@ describe('buildGraphqlTypes', () => { // Then await expect(got).resolves.toBeUndefined() + expect(packageManagerBinaryCommandForDirectory).not.toHaveBeenCalled() expect(exec).toHaveBeenCalledWith('custom-typegen', ['--output', 'types.ts'], { cwd: ourFunction.directory, stdout, diff --git a/packages/app/src/cli/services/function/build.ts b/packages/app/src/cli/services/function/build.ts index 7e8caf4f6f..547401eb03 100644 --- a/packages/app/src/cli/services/function/build.ts +++ b/packages/app/src/cli/services/function/build.ts @@ -24,6 +24,7 @@ import {renderTasks} from '@shopify/cli-kit/node/ui' import {pickBy} from '@shopify/cli-kit/common/object' import {runWithTimer} from '@shopify/cli-kit/node/metadata' import {AbortError} from '@shopify/cli-kit/node/error' +import {packageManagerBinaryCommandForDirectory} from '@shopify/cli-kit/node/node-package-manager' import {Writable} from 'stream' export const PREFERRED_FUNCTION_NPM_PACKAGE_MAJOR_VERSION = '2' @@ -143,8 +144,15 @@ export async function buildGraphqlTypes( ) } + const command = await packageManagerBinaryCommandForDirectory( + fun.directory, + 'graphql-code-generator', + '--config', + 'package.json', + ) + return runWithTimer('cmd_all_timing_network_ms')(async () => { - return exec('npm', ['exec', '--', 'graphql-code-generator', '--config', 'package.json'], { + return exec(command.command, command.args, { cwd: fun.directory, stderr: options.stderr, signal: options.signal, diff --git a/packages/cli-kit/src/public/node/node-package-manager.test.ts b/packages/cli-kit/src/public/node/node-package-manager.test.ts index 8516249743..b9c1448f5a 100644 --- a/packages/cli-kit/src/public/node/node-package-manager.test.ts +++ b/packages/cli-kit/src/public/node/node-package-manager.test.ts @@ -21,6 +21,10 @@ import { inferPackageManager, PackageManager, npmLockfile, + pnpmLockfile, + yarnLockfile, + bunLockfile, + packageManagerBinaryCommandForDirectory, } from './node-package-manager.js' import {captureOutput, exec} from './system.js' import {inTemporaryDirectory, mkdir, touchFile, writeFile} from './fs.js' @@ -917,6 +921,87 @@ describe('getPackageManager', () => { }) }) +describe('packageManagerBinaryCommandForDirectory', () => { + test('returns npm exec arguments for npm projects', async () => { + await inTemporaryDirectory(async (tmpDir) => { + await writePackageJSON(tmpDir, {name: 'mock name'}) + await writeFile(joinPath(tmpDir, npmLockfile), '') + + const command = await packageManagerBinaryCommandForDirectory( + tmpDir, + 'graphql-code-generator', + '--config', + 'package.json', + ) + + expect(command).toEqual({ + command: 'npm', + args: ['exec', '--', 'graphql-code-generator', '--config', 'package.json'], + }) + }) + }) + + test('returns pnpm exec arguments for nested pnpm workspace packages', async () => { + await inTemporaryDirectory(async (tmpDir) => { + await writePackageJSON(tmpDir, {name: 'root'}) + await writeFile(joinPath(tmpDir, pnpmLockfile), '') + const nested = joinPath(tmpDir, 'extensions', 'cart-transformer') + await mkdir(nested) + await writePackageJSON(nested, {name: 'cart-transformer'}) + + const command = await packageManagerBinaryCommandForDirectory( + nested, + 'graphql-code-generator', + '--config', + 'package.json', + ) + + expect(command).toEqual({ + command: 'pnpm', + args: ['exec', 'graphql-code-generator', '--config', 'package.json'], + }) + }) + }) + + test('returns yarn run arguments for yarn projects', async () => { + await inTemporaryDirectory(async (tmpDir) => { + await writePackageJSON(tmpDir, {name: 'mock name'}) + await writeFile(joinPath(tmpDir, yarnLockfile), '') + + const command = await packageManagerBinaryCommandForDirectory( + tmpDir, + 'graphql-code-generator', + '--config', + 'package.json', + ) + + expect(command).toEqual({ + command: 'yarn', + args: ['run', 'graphql-code-generator', '--config', 'package.json'], + }) + }) + }) + + test('returns bun x arguments for bun projects', async () => { + await inTemporaryDirectory(async (tmpDir) => { + await writePackageJSON(tmpDir, {name: 'mock name'}) + await writeFile(joinPath(tmpDir, bunLockfile), '') + + const command = await packageManagerBinaryCommandForDirectory( + tmpDir, + 'graphql-code-generator', + '--config', + 'package.json', + ) + + expect(command).toEqual({ + command: 'bun', + args: ['x', 'graphql-code-generator', '--config', 'package.json'], + }) + }) + }) +}) + describe('addNPMDependencies', () => { test('when using npm with multiple dependencies they should be installed one by one, adding --save-exact if needed', async () => { await inTemporaryDirectory(async (tmpDir) => { diff --git a/packages/cli-kit/src/public/node/node-package-manager.ts b/packages/cli-kit/src/public/node/node-package-manager.ts index b35afb8b6d..a8d5aa069c 100644 --- a/packages/cli-kit/src/public/node/node-package-manager.ts +++ b/packages/cli-kit/src/public/node/node-package-manager.ts @@ -137,6 +137,28 @@ export async function getPackageManager(fromDirectory: string): Promise { + const packageManager = await getPackageManager(directory) + + switch (packageManager) { + case 'npm': + return {command: 'npm', args: ['exec', '--', binary, ...args]} + case 'pnpm': + return {command: 'pnpm', args: ['exec', binary, ...args]} + case 'yarn': + return {command: 'yarn', args: ['run', binary, ...args]} + case 'bun': + return {command: 'bun', args: ['x', binary, ...args]} + case 'homebrew': + case 'unknown': + throw new UnknownPackageManagerError() + } +} + interface InstallNPMDependenciesRecursivelyOptions { /** * The dependency manager to use to install the dependencies.