Skip to content

Commit 08c1fba

Browse files
authored
Merge pull request #6870 from Shopify/functions-typegen-command-support
[Feature] Add build.typegen_command support for non-JS Shopify Functions
2 parents dfa66e1 + 64cb7a9 commit 08c1fba

12 files changed

Lines changed: 257 additions & 18 deletions

File tree

docs-shopify.dev/commands/app-function-typegen.doc.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'
33

44
const data: ReferenceEntityTemplateSchema = {
55
name: 'app function typegen',
6-
description: `Creates GraphQL types based on your [input query](/docs/apps/functions/input-output#input) for a function written in JavaScript.`,
7-
overviewPreviewDescription: `Generate GraphQL types for a JavaScript function.`,
6+
description: `Creates GraphQL types based on your [input query](/docs/apps/functions/input-output#input) for a function. Supports JavaScript functions out of the box, or any language via the \`build.typegen_command\` configuration.`,
7+
overviewPreviewDescription: `Generate GraphQL types for a function.`,
88
type: 'command',
99
isVisualComponent: false,
1010
defaultExample: {

docs-shopify.dev/generated/generated_docs_data.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2088,8 +2088,8 @@
20882088
},
20892089
{
20902090
"name": "app function typegen",
2091-
"description": "Creates GraphQL types based on your [input query](/docs/apps/functions/input-output#input) for a function written in JavaScript.",
2092-
"overviewPreviewDescription": "Generate GraphQL types for a JavaScript function.",
2091+
"description": "Creates GraphQL types based on your [input query](/docs/apps/functions/input-output#input) for a function. Supports JavaScript functions out of the box, or any language via the `build.typegen_command` configuration.",
2092+
"overviewPreviewDescription": "Generate GraphQL types for a function.",
20932093
"type": "command",
20942094
"isVisualComponent": false,
20952095
"defaultExample": {

packages/app/src/cli/commands/app/function/typegen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import {globalFlags} from '@shopify/cli-kit/node/cli'
77
import {renderSuccess} from '@shopify/cli-kit/node/ui'
88

99
export default class FunctionTypegen extends AppUnlinkedCommand {
10-
static summary = 'Generate GraphQL types for a JavaScript function.'
10+
static summary = 'Generate GraphQL types for a function.'
1111

12-
static descriptionWithMarkdown = `Creates GraphQL types based on your [input query](https://shopify.dev/docs/apps/functions/input-output#input) for a function written in JavaScript.`
12+
static descriptionWithMarkdown = `Creates GraphQL types based on your [input query](https://shopify.dev/docs/apps/functions/input-output#input) for a function. Supports JavaScript functions out of the box, or any language via the \`build.typegen_command\` configuration.`
1313

1414
static description = this.descriptionWithoutMarkdown()
1515

packages/app/src/cli/models/extensions/extension-instance.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,11 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
287287
return config.build?.command
288288
}
289289

290+
get typegenCommand() {
291+
const config = this.configuration as unknown as FunctionConfigType
292+
return config.build?.typegen_command
293+
}
294+
290295
/**
291296
* Default entry paths to be watched in a dev session.
292297
* It returns the entry source file path if defined,

packages/app/src/cli/models/extensions/specifications/function.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,6 +306,34 @@ describe('functionConfiguration', () => {
306306
})
307307
})
308308

309+
test('accepts configuration with typegen_command in build section', async () => {
310+
// Given
311+
const configWithTypegen = {
312+
name: 'function',
313+
type: 'function',
314+
metafields: [],
315+
description: 'my function',
316+
build: {
317+
command: 'zig build -Doptimize=ReleaseSmall',
318+
path: 'dist/index.wasm',
319+
wasm_opt: true,
320+
typegen_command: 'npx shopify-function-codegen --schema schema.graphql',
321+
},
322+
configuration_ui: false,
323+
api_version: '2022-07',
324+
}
325+
326+
// When
327+
const extension = await testFunctionExtension({
328+
dir: '/function',
329+
config: configWithTypegen as FunctionConfigType,
330+
})
331+
332+
// Then
333+
expect(extension.configuration.build?.typegen_command).toBe('npx shopify-function-codegen --schema schema.graphql')
334+
expect(extension.typegenCommand).toBe('npx shopify-function-codegen --schema schema.graphql')
335+
})
336+
309337
test('accepts configuration without build section', async () => {
310338
// Given
311339
const configWithoutBuild = {

packages/app/src/cli/models/extensions/specifications/function.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,10 @@ const FunctionExtensionSchema = BaseSchema.extend({
2727
path: zod.string().optional(),
2828
watch: zod.union([zod.string(), zod.string().array()]).optional(),
2929
wasm_opt: zod.boolean().optional().default(true),
30+
typegen_command: zod
31+
.string()
32+
.transform((value) => (value.trim() === '' ? undefined : value))
33+
.optional(),
3034
})
3135
.optional(),
3236
name: zod.string(),

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

Lines changed: 123 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import {buildFunctionExtension} from './extension.js'
22
import {testFunctionExtension} from '../../models/app/app.test-data.js'
3-
import {buildJSFunction, runWasmOpt, runTrampoline} from '../function/build.js'
3+
import {buildGraphqlTypes, buildJSFunction, runWasmOpt, runTrampoline} from '../function/build.js'
44
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
55
import {FunctionConfigType} from '../../models/extensions/specifications/function.js'
66
import {beforeEach, describe, expect, test, vi} from 'vitest'
@@ -254,6 +254,128 @@ describe('buildFunctionExtension', () => {
254254
expect(runWasmOpt).not.toHaveBeenCalled()
255255
})
256256

257+
test('runs typegen_command before build for non-JS function', async () => {
258+
// Given
259+
const configWithTypegen = {
260+
name: 'MyFunction',
261+
type: 'product_discounts',
262+
description: '',
263+
build: {
264+
command: 'make build',
265+
path: 'dist/index.wasm',
266+
wasm_opt: true,
267+
typegen_command: 'npx shopify-function-codegen --schema schema.graphql',
268+
},
269+
configuration_ui: true,
270+
api_version: '2022-07',
271+
metafields: [],
272+
}
273+
extension = await testFunctionExtension({config: configWithTypegen})
274+
275+
// When
276+
await expect(
277+
buildFunctionExtension(extension, {
278+
stdout,
279+
stderr,
280+
signal,
281+
app,
282+
environment: 'production',
283+
}),
284+
).resolves.toBeUndefined()
285+
286+
// Then
287+
expect(buildGraphqlTypes).toHaveBeenCalledWith(extension, {
288+
stdout,
289+
stderr,
290+
signal,
291+
app,
292+
environment: 'production',
293+
})
294+
expect(exec).toHaveBeenCalledWith('make', ['build'], {
295+
stdout,
296+
stderr,
297+
cwd: extension.directory,
298+
signal,
299+
})
300+
})
301+
302+
test('runs typegen_command before build for JS function with custom build command', async () => {
303+
// Given
304+
const configWithTypegen = {
305+
name: 'MyFunction',
306+
type: 'product_discounts',
307+
description: '',
308+
build: {
309+
command: 'make build',
310+
path: 'dist/index.wasm',
311+
wasm_opt: true,
312+
typegen_command: 'custom-typegen --output types.ts',
313+
},
314+
configuration_ui: true,
315+
api_version: '2022-07',
316+
metafields: [],
317+
}
318+
extension = await testFunctionExtension({config: configWithTypegen, entryPath: 'src/index.js'})
319+
320+
// When
321+
await expect(
322+
buildFunctionExtension(extension, {
323+
stdout,
324+
stderr,
325+
signal,
326+
app,
327+
environment: 'production',
328+
}),
329+
).resolves.toBeUndefined()
330+
331+
// Then
332+
expect(buildGraphqlTypes).toHaveBeenCalledWith(extension, {
333+
stdout,
334+
stderr,
335+
signal,
336+
app,
337+
environment: 'production',
338+
})
339+
expect(exec).toHaveBeenCalledWith('make', ['build'], {
340+
stdout,
341+
stderr,
342+
cwd: extension.directory,
343+
signal,
344+
})
345+
})
346+
347+
test('does not run typegen when typegen_command is not set', async () => {
348+
// Given
349+
const configWithoutTypegen = {
350+
name: 'MyFunction',
351+
type: 'product_discounts',
352+
description: '',
353+
build: {
354+
command: 'make build',
355+
path: 'dist/index.wasm',
356+
wasm_opt: true,
357+
},
358+
configuration_ui: true,
359+
api_version: '2022-07',
360+
metafields: [],
361+
}
362+
extension = await testFunctionExtension({config: configWithoutTypegen})
363+
364+
// When
365+
await expect(
366+
buildFunctionExtension(extension, {
367+
stdout,
368+
stderr,
369+
signal,
370+
app,
371+
environment: 'production',
372+
}),
373+
).resolves.toBeUndefined()
374+
375+
// Then
376+
expect(buildGraphqlTypes).not.toHaveBeenCalled()
377+
})
378+
257379
test('handles function with build config but undefined path', async () => {
258380
// Given
259381
const configWithoutPath = {

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {runThemeCheck} from './theme-check.js'
22
import {AppInterface} from '../../models/app/app.js'
33
import {bundleExtension} from '../extensions/bundle.js'
4-
import {buildJSFunction, runTrampoline, runWasmOpt} from '../function/build.js'
4+
import {buildGraphqlTypes, buildJSFunction, runTrampoline, runWasmOpt} from '../function/build.js'
55
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
66
import {FunctionConfigType} from '../../models/extensions/specifications/function.js'
77
import {exec} from '@shopify/cli-kit/node/system'
@@ -202,6 +202,9 @@ export async function bundleFunctionExtension(wasmPath: string, bundlePath: stri
202202

203203
async function runCommandOrBuildJSFunction(extension: ExtensionInstance, options: BuildFunctionExtensionOptions) {
204204
if (extension.buildCommand) {
205+
if (extension.typegenCommand) {
206+
await buildGraphqlTypes(extension, options)
207+
}
205208
return runCommand(extension.buildCommand, extension, options)
206209
} else {
207210
return buildJSFunction(extension as ExtensionInstance<FunctionConfigType>, options)
@@ -223,6 +226,9 @@ async function buildOtherFunction(extension: ExtensionInstance, options: BuildFu
223226
`)
224227
throw new AbortSilentError()
225228
}
229+
if (extension.typegenCommand) {
230+
await buildGraphqlTypes(extension, options)
231+
}
226232
return runCommand(extension.buildCommand, extension, options)
227233
}
228234

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

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ describe('buildGraphqlTypes', () => {
8585
})
8686
})
8787

88-
test('errors if function is not a JS function', async () => {
88+
test('errors if function is not a JS function and no typegen_command', async () => {
8989
// Given
9090
const ourFunction = await testFunctionExtension()
9191
ourFunction.entrySourceFilePath = 'src/main.rs'
@@ -94,7 +94,67 @@ describe('buildGraphqlTypes', () => {
9494
const got = buildGraphqlTypes(ourFunction, {stdout, stderr, signal, app})
9595

9696
// Then
97-
await expect(got).rejects.toThrow(/GraphQL types can only be built for JavaScript functions/)
97+
await expect(got).rejects.toThrow(/No typegen_command specified/)
98+
})
99+
100+
test('runs custom typegen_command when provided', async () => {
101+
// Given
102+
const ourFunction = await testFunctionExtension({
103+
config: {
104+
name: 'test function',
105+
type: 'order_discounts',
106+
build: {
107+
command: 'zig build',
108+
wasm_opt: true,
109+
typegen_command: 'npx shopify-function-codegen --schema schema.graphql',
110+
},
111+
configuration_ui: true,
112+
api_version: '2024-01',
113+
},
114+
})
115+
ourFunction.entrySourceFilePath = 'src/main.rs'
116+
117+
// When
118+
const got = buildGraphqlTypes(ourFunction, {stdout, stderr, signal, app})
119+
120+
// Then
121+
await expect(got).resolves.toBeUndefined()
122+
expect(exec).toHaveBeenCalledWith('npx', ['shopify-function-codegen', '--schema', 'schema.graphql'], {
123+
cwd: ourFunction.directory,
124+
stdout,
125+
stderr,
126+
signal,
127+
})
128+
})
129+
130+
test('runs custom typegen_command for JS functions when provided', async () => {
131+
// Given
132+
const ourFunction = await testFunctionExtension({
133+
entryPath: 'src/index.js',
134+
config: {
135+
name: 'test function',
136+
type: 'order_discounts',
137+
build: {
138+
command: 'echo "hello"',
139+
wasm_opt: true,
140+
typegen_command: 'custom-typegen --output types.ts',
141+
},
142+
configuration_ui: true,
143+
api_version: '2024-01',
144+
},
145+
})
146+
147+
// When
148+
const got = buildGraphqlTypes(ourFunction, {stdout, stderr, signal, app})
149+
150+
// Then
151+
await expect(got).resolves.toBeUndefined()
152+
expect(exec).toHaveBeenCalledWith('custom-typegen', ['--output', 'types.ts'], {
153+
cwd: ourFunction.directory,
154+
stdout,
155+
stderr,
156+
signal,
157+
})
98158
})
99159
})
100160

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

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,25 @@ async function buildJSFunctionWithTasks(
122122
}
123123

124124
export async function buildGraphqlTypes(
125-
fun: {directory: string; isJavaScript: boolean},
125+
fun: {directory: string; isJavaScript: boolean; typegenCommand?: string},
126126
options: JSFunctionBuildOptions,
127127
) {
128+
if (fun.typegenCommand) {
129+
const commandComponents = fun.typegenCommand.split(' ')
130+
return runWithTimer('cmd_all_timing_network_ms')(async () => {
131+
return exec(commandComponents[0]!, commandComponents.slice(1), {
132+
cwd: fun.directory,
133+
stdout: options.stdout,
134+
stderr: options.stderr,
135+
signal: options.signal,
136+
})
137+
})
138+
}
139+
128140
if (!fun.isJavaScript) {
129-
throw new AbortError('GraphQL types can only be built for JavaScript functions')
141+
throw new AbortError(
142+
'No typegen_command specified. Set build.typegen_command in your function extension TOML to generate GraphQL types for non-JavaScript functions.',
143+
)
130144
}
131145

132146
return runWithTimer('cmd_all_timing_network_ms')(async () => {

0 commit comments

Comments
 (0)