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
4 changes: 2 additions & 2 deletions docs-shopify.dev/commands/app-function-typegen.doc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import {ReferenceEntityTemplateSchema} from '@shopify/generate-docs'

const data: ReferenceEntityTemplateSchema = {
name: 'app function typegen',
description: `Creates GraphQL types based on your [input query](/docs/apps/functions/input-output#input) for a function written in JavaScript.`,
overviewPreviewDescription: `Generate GraphQL types for a JavaScript function.`,
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.`,
overviewPreviewDescription: `Generate GraphQL types for a function.`,
type: 'command',
isVisualComponent: false,
defaultExample: {
Expand Down
4 changes: 2 additions & 2 deletions docs-shopify.dev/generated/generated_docs_data.json
Original file line number Diff line number Diff line change
Expand Up @@ -2088,8 +2088,8 @@
},
{
"name": "app function typegen",
"description": "Creates GraphQL types based on your [input query](/docs/apps/functions/input-output#input) for a function written in JavaScript.",
"overviewPreviewDescription": "Generate GraphQL types for a JavaScript function.",
"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.",
"overviewPreviewDescription": "Generate GraphQL types for a function.",
"type": "command",
"isVisualComponent": false,
"defaultExample": {
Expand Down
4 changes: 2 additions & 2 deletions packages/app/src/cli/commands/app/function/typegen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ import {globalFlags} from '@shopify/cli-kit/node/cli'
import {renderSuccess} from '@shopify/cli-kit/node/ui'

export default class FunctionTypegen extends AppUnlinkedCommand {
static summary = 'Generate GraphQL types for a JavaScript function.'
static summary = 'Generate GraphQL types for a function.'

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

static description = this.descriptionWithoutMarkdown()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,11 @@ export class ExtensionInstance<TConfiguration extends BaseConfigType = BaseConfi
return config.build?.command
}

get typegenCommand() {
const config = this.configuration as unknown as FunctionConfigType
return config.build?.typegen_command
}

/**
* Default entry paths to be watched in a dev session.
* It returns the entry source file path if defined,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,34 @@ describe('functionConfiguration', () => {
})
})

test('accepts configuration with typegen_command in build section', async () => {
// Given
const configWithTypegen = {
name: 'function',
type: 'function',
metafields: [],
description: 'my function',
build: {
command: 'zig build -Doptimize=ReleaseSmall',
path: 'dist/index.wasm',
wasm_opt: true,
typegen_command: 'npx shopify-function-codegen --schema schema.graphql',
},
configuration_ui: false,
api_version: '2022-07',
}

// When
const extension = await testFunctionExtension({
dir: '/function',
config: configWithTypegen as FunctionConfigType,
})

// Then
expect(extension.configuration.build?.typegen_command).toBe('npx shopify-function-codegen --schema schema.graphql')
expect(extension.typegenCommand).toBe('npx shopify-function-codegen --schema schema.graphql')
})

test('accepts configuration without build section', async () => {
// Given
const configWithoutBuild = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ const FunctionExtensionSchema = BaseSchema.extend({
path: zod.string().optional(),
watch: zod.union([zod.string(), zod.string().array()]).optional(),
wasm_opt: zod.boolean().optional().default(true),
typegen_command: zod
.string()
.transform((value) => (value.trim() === '' ? undefined : value))
.optional(),
})
.optional(),
name: zod.string(),
Expand Down
124 changes: 123 additions & 1 deletion packages/app/src/cli/services/build/extension.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import {buildFunctionExtension} from './extension.js'
import {testFunctionExtension} from '../../models/app/app.test-data.js'
import {buildJSFunction, runWasmOpt, runTrampoline} from '../function/build.js'
import {buildGraphqlTypes, buildJSFunction, runWasmOpt, runTrampoline} from '../function/build.js'
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
import {FunctionConfigType} from '../../models/extensions/specifications/function.js'
import {beforeEach, describe, expect, test, vi} from 'vitest'
Expand Down Expand Up @@ -254,6 +254,128 @@ describe('buildFunctionExtension', () => {
expect(runWasmOpt).not.toHaveBeenCalled()
})

test('runs typegen_command before build for non-JS function', async () => {
// Given
const configWithTypegen = {
name: 'MyFunction',
type: 'product_discounts',
description: '',
build: {
command: 'make build',
path: 'dist/index.wasm',
wasm_opt: true,
typegen_command: 'npx shopify-function-codegen --schema schema.graphql',
},
configuration_ui: true,
api_version: '2022-07',
metafields: [],
}
extension = await testFunctionExtension({config: configWithTypegen})

// When
await expect(
buildFunctionExtension(extension, {
stdout,
stderr,
signal,
app,
environment: 'production',
}),
).resolves.toBeUndefined()

// Then
expect(buildGraphqlTypes).toHaveBeenCalledWith(extension, {
stdout,
stderr,
signal,
app,
environment: 'production',
})
expect(exec).toHaveBeenCalledWith('make', ['build'], {
stdout,
stderr,
cwd: extension.directory,
signal,
})
})

test('runs typegen_command before build for JS function with custom build command', async () => {
// Given
const configWithTypegen = {
name: 'MyFunction',
type: 'product_discounts',
description: '',
build: {
command: 'make build',
path: 'dist/index.wasm',
wasm_opt: true,
typegen_command: 'custom-typegen --output types.ts',
},
configuration_ui: true,
api_version: '2022-07',
metafields: [],
}
extension = await testFunctionExtension({config: configWithTypegen, entryPath: 'src/index.js'})

// When
await expect(
buildFunctionExtension(extension, {
stdout,
stderr,
signal,
app,
environment: 'production',
}),
).resolves.toBeUndefined()

// Then
expect(buildGraphqlTypes).toHaveBeenCalledWith(extension, {
stdout,
stderr,
signal,
app,
environment: 'production',
})
expect(exec).toHaveBeenCalledWith('make', ['build'], {
stdout,
stderr,
cwd: extension.directory,
signal,
})
})

test('does not run typegen when typegen_command is not set', async () => {
// Given
const configWithoutTypegen = {
name: 'MyFunction',
type: 'product_discounts',
description: '',
build: {
command: 'make build',
path: 'dist/index.wasm',
wasm_opt: true,
},
configuration_ui: true,
api_version: '2022-07',
metafields: [],
}
extension = await testFunctionExtension({config: configWithoutTypegen})

// When
await expect(
buildFunctionExtension(extension, {
stdout,
stderr,
signal,
app,
environment: 'production',
}),
).resolves.toBeUndefined()

// Then
expect(buildGraphqlTypes).not.toHaveBeenCalled()
})

test('handles function with build config but undefined path', async () => {
// Given
const configWithoutPath = {
Expand Down
8 changes: 7 additions & 1 deletion packages/app/src/cli/services/build/extension.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {runThemeCheck} from './theme-check.js'
import {AppInterface} from '../../models/app/app.js'
import {bundleExtension} from '../extensions/bundle.js'
import {buildJSFunction, runTrampoline, runWasmOpt} from '../function/build.js'
import {buildGraphqlTypes, buildJSFunction, runTrampoline, runWasmOpt} from '../function/build.js'
import {ExtensionInstance} from '../../models/extensions/extension-instance.js'
import {FunctionConfigType} from '../../models/extensions/specifications/function.js'
import {exec} from '@shopify/cli-kit/node/system'
Expand Down Expand Up @@ -202,6 +202,9 @@ export async function bundleFunctionExtension(wasmPath: string, bundlePath: stri

async function runCommandOrBuildJSFunction(extension: ExtensionInstance, options: BuildFunctionExtensionOptions) {
if (extension.buildCommand) {
if (extension.typegenCommand) {
await buildGraphqlTypes(extension, options)
}
return runCommand(extension.buildCommand, extension, options)
} else {
return buildJSFunction(extension as ExtensionInstance<FunctionConfigType>, options)
Expand All @@ -223,6 +226,9 @@ async function buildOtherFunction(extension: ExtensionInstance, options: BuildFu
`)
throw new AbortSilentError()
}
if (extension.typegenCommand) {
await buildGraphqlTypes(extension, options)
}
return runCommand(extension.buildCommand, extension, options)
}

Expand Down
62 changes: 60 additions & 2 deletions packages/app/src/cli/services/function/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ describe('buildGraphqlTypes', () => {
})
})

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

// Then
await expect(got).rejects.toThrow(/GraphQL types can only be built for JavaScript functions/)
await expect(got).rejects.toThrow(/No typegen_command specified/)
})

test('runs custom typegen_command when provided', async () => {
// Given
const ourFunction = await testFunctionExtension({
config: {
name: 'test function',
type: 'order_discounts',
build: {
command: 'zig build',
wasm_opt: true,
typegen_command: 'npx shopify-function-codegen --schema schema.graphql',
},
configuration_ui: true,
api_version: '2024-01',
},
})
ourFunction.entrySourceFilePath = 'src/main.rs'

// When
const got = buildGraphqlTypes(ourFunction, {stdout, stderr, signal, app})

// Then
await expect(got).resolves.toBeUndefined()
expect(exec).toHaveBeenCalledWith('npx', ['shopify-function-codegen', '--schema', 'schema.graphql'], {
cwd: ourFunction.directory,
stderr,
signal,
})
})

test('runs custom typegen_command for JS functions when provided', async () => {
// Given
const ourFunction = await testFunctionExtension({
entryPath: 'src/index.js',
config: {
name: 'test function',
type: 'order_discounts',
build: {
command: 'echo "hello"',
wasm_opt: true,
typegen_command: 'custom-typegen --output types.ts',
},
configuration_ui: true,
api_version: '2024-01',
},
})

// When
const got = buildGraphqlTypes(ourFunction, {stdout, stderr, signal, app})

// Then
await expect(got).resolves.toBeUndefined()
expect(exec).toHaveBeenCalledWith('custom-typegen', ['--output', 'types.ts'], {
cwd: ourFunction.directory,
stderr,
signal,
})
})
})

Expand Down
17 changes: 15 additions & 2 deletions packages/app/src/cli/services/function/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,11 +123,24 @@ async function buildJSFunctionWithTasks(
}

export async function buildGraphqlTypes(
fun: {directory: string; isJavaScript: boolean},
fun: {directory: string; isJavaScript: boolean; typegenCommand?: string},
options: JSFunctionBuildOptions,
) {
if (fun.typegenCommand) {
const commandComponents = fun.typegenCommand.split(' ')
return runWithTimer('cmd_all_timing_network_ms')(async () => {
return exec(commandComponents[0]!, commandComponents.slice(1), {
cwd: fun.directory,
stderr: options.stderr,
signal: options.signal,
})
})
}

if (!fun.isJavaScript) {
throw new AbortError('GraphQL types can only be built for JavaScript functions')
throw new AbortError(
'No typegen_command specified. Set build.typegen_command in your function extension TOML to generate GraphQL types for non-JavaScript functions.',
)
}

return runWithTimer('cmd_all_timing_network_ms')(async () => {
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -662,7 +662,7 @@ DESCRIPTION

## `shopify app function typegen`

Generate GraphQL types for a JavaScript function.
Generate GraphQL types for a function.

```
USAGE
Expand All @@ -678,10 +678,10 @@ FLAGS
--verbose [env: SHOPIFY_FLAG_VERBOSE] Increase the verbosity of the output.

DESCRIPTION
Generate GraphQL types for a JavaScript function.
Generate GraphQL types for a function.

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

## `shopify app generate extension`
Expand Down
Loading
Loading