Skip to content

Commit 7cf64a7

Browse files
committed
Use function directory package manager for GraphQL typegen
Detect the package manager from the function directory when running the default JavaScript GraphQL type generation path, instead of hardcoding npm.\n\nKeep the broader getPackageManager() behavior unchanged by adding a narrow cli-kit helper that resolves the correct package-manager-specific binary invocation for a project directory.
1 parent a66b1c7 commit 7cf64a7

File tree

4 files changed

+281
-12
lines changed

4 files changed

+281
-12
lines changed

packages/app/src/cli/services/function/build.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,20 @@ import {
2222
import {testApp, testFunctionExtension} from '../../models/app/app.test-data.js'
2323
import {beforeEach, describe, expect, test, vi} from 'vitest'
2424
import {exec} from '@shopify/cli-kit/node/system'
25+
import {packageManagerBinaryCommandForDirectory} from '@shopify/cli-kit/node/node-package-manager'
2526
import {dirname, joinPath} from '@shopify/cli-kit/node/path'
2627
import {inTemporaryDirectory, mkdir, readFileSync, writeFile, removeFile} from '@shopify/cli-kit/node/fs'
2728
import {build as esBuild} from 'esbuild'
2829

2930
vi.mock('@shopify/cli-kit/node/fs')
3031
vi.mock('@shopify/cli-kit/node/system')
32+
vi.mock('@shopify/cli-kit/node/node-package-manager', async () => {
33+
const actual: any = await vi.importActual('@shopify/cli-kit/node/node-package-manager')
34+
return {
35+
...actual,
36+
packageManagerBinaryCommandForDirectory: vi.fn(),
37+
}
38+
})
3139

3240
vi.mock('./binaries.js', async (importOriginal) => {
3341
const actual: any = await importOriginal()
@@ -76,6 +84,10 @@ beforeEach(async () => {
7684
stderr = {write: vi.fn()}
7785
stdout = {write: vi.fn()}
7886
signal = vi.fn()
87+
vi.mocked(packageManagerBinaryCommandForDirectory).mockResolvedValue({
88+
command: 'npm',
89+
args: ['exec', '--', 'graphql-code-generator', '--config', 'package.json'],
90+
})
7991
})
8092

8193
describe('buildGraphqlTypes', () => {
@@ -88,13 +100,40 @@ describe('buildGraphqlTypes', () => {
88100

89101
// Then
90102
await expect(got).resolves.toBeUndefined()
103+
expect(packageManagerBinaryCommandForDirectory).toHaveBeenCalledTimes(1)
104+
expect(packageManagerBinaryCommandForDirectory).toHaveBeenCalledWith(
105+
ourFunction.directory,
106+
'graphql-code-generator',
107+
'--config',
108+
'package.json',
109+
)
91110
expect(exec).toHaveBeenCalledWith('npm', ['exec', '--', 'graphql-code-generator', '--config', 'package.json'], {
92111
cwd: ourFunction.directory,
93112
stderr,
94113
signal,
95114
})
96115
})
97116

117+
test('generate types executes the command returned by the shared helper', {timeout: 20000}, async () => {
118+
// Given
119+
const ourFunction = await testFunctionExtension({entryPath: 'src/index.js'})
120+
vi.mocked(packageManagerBinaryCommandForDirectory).mockResolvedValue({
121+
command: 'pnpm',
122+
args: ['exec', 'graphql-code-generator', '--config', 'package.json'],
123+
})
124+
125+
// When
126+
const got = buildGraphqlTypes(ourFunction, {stdout, stderr, signal, app})
127+
128+
// Then
129+
await expect(got).resolves.toBeUndefined()
130+
expect(exec).toHaveBeenCalledWith('pnpm', ['exec', 'graphql-code-generator', '--config', 'package.json'], {
131+
cwd: ourFunction.directory,
132+
stderr,
133+
signal,
134+
})
135+
})
136+
98137
test('errors if function is not a JS function and no typegen_command', async () => {
99138
// Given
100139
const ourFunction = await testFunctionExtension()
@@ -105,6 +144,7 @@ describe('buildGraphqlTypes', () => {
105144

106145
// Then
107146
await expect(got).rejects.toThrow(/No typegen_command specified/)
147+
expect(packageManagerBinaryCommandForDirectory).not.toHaveBeenCalled()
108148
})
109149

110150
test('runs custom typegen_command when provided', async () => {
@@ -129,6 +169,7 @@ describe('buildGraphqlTypes', () => {
129169

130170
// Then
131171
await expect(got).resolves.toBeUndefined()
172+
expect(packageManagerBinaryCommandForDirectory).not.toHaveBeenCalled()
132173
expect(exec).toHaveBeenCalledWith('npx', ['shopify-function-codegen', '--schema', 'schema.graphql'], {
133174
cwd: ourFunction.directory,
134175
stdout,
@@ -159,6 +200,7 @@ describe('buildGraphqlTypes', () => {
159200

160201
// Then
161202
await expect(got).resolves.toBeUndefined()
203+
expect(packageManagerBinaryCommandForDirectory).not.toHaveBeenCalled()
162204
expect(exec).toHaveBeenCalledWith('custom-typegen', ['--output', 'types.ts'], {
163205
cwd: ourFunction.directory,
164206
stdout,

packages/app/src/cli/services/function/build.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import {renderTasks} from '@shopify/cli-kit/node/ui'
2424
import {pickBy} from '@shopify/cli-kit/common/object'
2525
import {runWithTimer} from '@shopify/cli-kit/node/metadata'
2626
import {AbortError} from '@shopify/cli-kit/node/error'
27+
import {packageManagerBinaryCommandForDirectory} from '@shopify/cli-kit/node/node-package-manager'
2728
import {Writable} from 'stream'
2829

2930
export const PREFERRED_FUNCTION_NPM_PACKAGE_MAJOR_VERSION = '2'
@@ -143,8 +144,15 @@ export async function buildGraphqlTypes(
143144
)
144145
}
145146

147+
const command = await packageManagerBinaryCommandForDirectory(
148+
fun.directory,
149+
'graphql-code-generator',
150+
'--config',
151+
'package.json',
152+
)
153+
146154
return runWithTimer('cmd_all_timing_network_ms')(async () => {
147-
return exec('npm', ['exec', '--', 'graphql-code-generator', '--config', 'package.json'], {
155+
return exec(command.command, command.args, {
148156
cwd: fun.directory,
149157
stderr: options.stderr,
150158
signal: options.signal,

packages/cli-kit/src/public/node/node-package-manager.test.ts

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
addResolutionOrOverride,
1313
writePackageJSON,
1414
getPackageManager,
15+
packageManagerBinaryCommandForDirectory,
1516
installNPMDependenciesRecursively,
1617
addNPMDependencies,
1718
DependencyVersion,
@@ -892,6 +893,21 @@ describe('getPackageManager', () => {
892893
})
893894
})
894895

896+
test('finds if bun is being used from bun.lock', async () => {
897+
await inTemporaryDirectory(async (tmpDir) => {
898+
// Given
899+
await writePackageJSON(tmpDir, {name: 'mock name'})
900+
await writeFile(joinPath(tmpDir, 'bun.lock'), '')
901+
mockedCaptureOutput.mockReturnValueOnce(Promise.resolve(tmpDir))
902+
903+
// When
904+
const packageManager = await getPackageManager(tmpDir)
905+
906+
// Then
907+
expect(packageManager).toEqual('bun')
908+
})
909+
})
910+
895911
test('falls back to packageManagerFromUserAgent when npm prefix fails', async () => {
896912
await inTemporaryDirectory(async (tmpDir) => {
897913
// Given
@@ -927,6 +943,133 @@ describe('getPackageManager', () => {
927943
})
928944
})
929945

946+
describe('packageManagerBinaryCommandForDirectory', () => {
947+
test('uses npm exec with -- for npm', async () => {
948+
await inTemporaryDirectory(async (tmpDir) => {
949+
await writePackageJSON(tmpDir, {name: 'mock name'})
950+
951+
await expect(
952+
packageManagerBinaryCommandForDirectory(tmpDir, 'graphql-code-generator', '--config', 'package.json'),
953+
).resolves.toEqual({
954+
command: 'npm',
955+
args: ['exec', '--', 'graphql-code-generator', '--config', 'package.json'],
956+
})
957+
})
958+
})
959+
960+
test('uses exec without -- for pnpm when detected from an ancestor workspace marker', async () => {
961+
await inTemporaryDirectory(async (tmpDir) => {
962+
await writePackageJSON(tmpDir, {name: 'app-root'})
963+
await writeFile(joinPath(tmpDir, 'pnpm-workspace.yaml'), '')
964+
const extensionDirectory = joinPath(tmpDir, 'extensions', 'my-function')
965+
await mkdir(extensionDirectory)
966+
await writePackageJSON(extensionDirectory, {name: 'my-function'})
967+
968+
await expect(
969+
packageManagerBinaryCommandForDirectory(
970+
extensionDirectory,
971+
'graphql-code-generator',
972+
'--config',
973+
'package.json',
974+
),
975+
).resolves.toEqual({
976+
command: 'pnpm',
977+
args: ['exec', 'graphql-code-generator', '--config', 'package.json'],
978+
})
979+
})
980+
})
981+
982+
test('uses yarn run when detected from yarn.lock', async () => {
983+
await inTemporaryDirectory(async (tmpDir) => {
984+
await writePackageJSON(tmpDir, {name: 'mock name'})
985+
await writeFile(joinPath(tmpDir, 'yarn.lock'), '')
986+
987+
await expect(
988+
packageManagerBinaryCommandForDirectory(tmpDir, 'graphql-code-generator', '--config', 'package.json'),
989+
).resolves.toEqual({
990+
command: 'yarn',
991+
args: ['run', 'graphql-code-generator', '--config', 'package.json'],
992+
})
993+
})
994+
})
995+
996+
test('uses bun x for bun when detected from bun.lock', async () => {
997+
await inTemporaryDirectory(async (tmpDir) => {
998+
await writePackageJSON(tmpDir, {name: 'mock name'})
999+
await writeFile(joinPath(tmpDir, 'bun.lock'), '')
1000+
1001+
await expect(
1002+
packageManagerBinaryCommandForDirectory(tmpDir, 'graphql-code-generator', '--config', 'package.json'),
1003+
).resolves.toEqual({
1004+
command: 'bun',
1005+
args: ['x', 'graphql-code-generator', '--config', 'package.json'],
1006+
})
1007+
})
1008+
})
1009+
1010+
test('uses bun x for bun when detected from bun.lockb', async () => {
1011+
await inTemporaryDirectory(async (tmpDir) => {
1012+
await writePackageJSON(tmpDir, {name: 'mock name'})
1013+
await writeFile(joinPath(tmpDir, 'bun.lockb'), '')
1014+
1015+
await expect(
1016+
packageManagerBinaryCommandForDirectory(tmpDir, 'graphql-code-generator', '--config', 'package.json'),
1017+
).resolves.toEqual({
1018+
command: 'bun',
1019+
args: ['x', 'graphql-code-generator', '--config', 'package.json'],
1020+
})
1021+
})
1022+
})
1023+
1024+
test('falls back to yarn run when the user agent is yarn', async () => {
1025+
await inTemporaryDirectory(async (tmpDir) => {
1026+
const extensionDirectory = joinPath(tmpDir, 'subdir')
1027+
await mkdir(extensionDirectory)
1028+
vi.stubEnv('npm_config_user_agent', 'yarn/1.22.0')
1029+
1030+
try {
1031+
await expect(
1032+
packageManagerBinaryCommandForDirectory(
1033+
extensionDirectory,
1034+
'graphql-code-generator',
1035+
'--config',
1036+
'package.json',
1037+
),
1038+
).resolves.toEqual({
1039+
command: 'yarn',
1040+
args: ['run', 'graphql-code-generator', '--config', 'package.json'],
1041+
})
1042+
} finally {
1043+
vi.unstubAllEnvs()
1044+
}
1045+
})
1046+
})
1047+
1048+
test('falls back to npm when no package manager markers or user agent are available', async () => {
1049+
await inTemporaryDirectory(async (tmpDir) => {
1050+
const extensionDirectory = joinPath(tmpDir, 'subdir')
1051+
await mkdir(extensionDirectory)
1052+
vi.stubEnv('npm_config_user_agent', '')
1053+
1054+
try {
1055+
await expect(
1056+
packageManagerBinaryCommandForDirectory(
1057+
extensionDirectory,
1058+
'graphql-code-generator',
1059+
'--config',
1060+
'package.json',
1061+
),
1062+
).resolves.toEqual({
1063+
command: 'npm',
1064+
args: ['exec', '--', 'graphql-code-generator', '--config', 'package.json'],
1065+
})
1066+
} finally {
1067+
vi.unstubAllEnvs()
1068+
}
1069+
})
1070+
})
1071+
})
1072+
9301073
describe('addNPMDependencies', () => {
9311074
test('when using npm with multiple dependencies they should be installed one by one, adding --save-exact if needed', async () => {
9321075
await inTemporaryDirectory(async (tmpDir) => {

0 commit comments

Comments
 (0)