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
56 changes: 55 additions & 1 deletion packages/app/src/cli/services/build/extension.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,15 @@ import {beforeEach, describe, expect, test, vi} from 'vitest'
import {exec} from '@shopify/cli-kit/node/system'
import lockfile from 'proper-lockfile'
import {AbortError} from '@shopify/cli-kit/node/error'
import {fileExistsSync, touchFile, writeFile} from '@shopify/cli-kit/node/fs'
import {fileExistsSync, readFile, touchFile, writeFile} from '@shopify/cli-kit/node/fs'
import {joinPath} from '@shopify/cli-kit/node/path'
import {outputWarn} from '@shopify/cli-kit/node/output'

vi.mock('@shopify/cli-kit/node/system')
vi.mock('../function/build.js')
vi.mock('proper-lockfile')
vi.mock('@shopify/cli-kit/node/fs')
vi.mock('@shopify/cli-kit/node/output')

describe('buildFunctionExtension', () => {
let extension: ExtensionInstance<FunctionConfigType>
Expand Down Expand Up @@ -439,4 +441,56 @@ describe('buildFunctionExtension', () => {
expect(touchFile).not.toHaveBeenCalled()
expect(writeFile).not.toHaveBeenCalled()
})

describe('schema version mismatch warning', () => {
function mockSchemaFile(content: string) {
vi.mocked(readFile).mockResolvedValueOnce(content as any)
}

function mockSchemaNotFound() {
const err = new Error('ENOENT') as NodeJS.ErrnoException
err.code = 'ENOENT'
vi.mocked(readFile).mockRejectedValueOnce(err)
}
Comment on lines +450 to +454
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tests cover ENOENT, match, mismatch, and “no directive”, but there’s no assertion for non-ENOENT read failures. Adding a test that readFile rejects with something like EACCES/EPERM and ensuring the error is propagated would prevent regressions (especially if warnIfSchemaMismatch is tightened to only ignore ENOENT).

Copilot uses AI. Check for mistakes.

test('warns when schema version does not match toml version', async () => {
vi.mocked(outputWarn).mockReset()
mockSchemaFile('schema @apiVersion(version: "2025-01") {\n query: QueryRoot\n}')

await buildFunctionExtension(extension, {stdout, stderr, signal, app, environment: 'production'})

expect(outputWarn).toHaveBeenCalledWith(
expect.stringContaining(
'schema.graphql was generated for API version 2025-01 but your extension targets 2022-07',
),
)
})

test('does not warn when schema version matches toml version', async () => {
vi.mocked(outputWarn).mockReset()
mockSchemaFile('schema @apiVersion(version: "2022-07") {\n query: QueryRoot\n}')

await buildFunctionExtension(extension, {stdout, stderr, signal, app, environment: 'production'})

expect(outputWarn).not.toHaveBeenCalled()
})

test('does not warn when schema has no apiVersion directive', async () => {
vi.mocked(outputWarn).mockReset()
mockSchemaFile('type Query { shop: Shop }')

await buildFunctionExtension(extension, {stdout, stderr, signal, app, environment: 'production'})

expect(outputWarn).not.toHaveBeenCalled()
})

test('does not warn when schema file does not exist', async () => {
vi.mocked(outputWarn).mockReset()
mockSchemaNotFound()

await buildFunctionExtension(extension, {stdout, stderr, signal, app, environment: 'production'})

expect(outputWarn).not.toHaveBeenCalled()
})
})
})
28 changes: 27 additions & 1 deletion packages/app/src/cli/services/build/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import {AbortSignal} from '@shopify/cli-kit/node/abort'
import {AbortError, AbortSilentError} from '@shopify/cli-kit/node/error'
import lockfile from 'proper-lockfile'
import {dirname, joinPath} from '@shopify/cli-kit/node/path'
import {outputDebug} from '@shopify/cli-kit/node/output'
import {outputDebug, outputWarn} from '@shopify/cli-kit/node/output'
import {readFile, touchFile, writeFile, fileExistsSync} from '@shopify/cli-kit/node/fs'
import {Writable} from 'stream'

Expand Down Expand Up @@ -124,6 +124,8 @@ export async function buildFunctionExtension(
extension: ExtensionInstance,
options: BuildFunctionExtensionOptions,
): Promise<void> {
await warnIfSchemaMismatch(extension as ExtensionInstance<FunctionConfigType>)

const lockfilePath = joinPath(extension.directory, '.build-lock')
let releaseLock
try {
Expand Down Expand Up @@ -225,6 +227,30 @@ async function buildOtherFunction(extension: ExtensionInstance, options: BuildFu
return runCommand(extension.buildCommand, extension, options)
}

const API_VERSION_DIRECTIVE_RE = /@apiVersion\(version:\s*"([^"]+)"\)/
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

API_VERSION_DIRECTIVE_RE requires version: with no whitespace before the colon. If the server ever formats this as version : "YYYY-MM" (still valid GraphQL), the warning will be skipped. Making the regex tolerant of optional whitespace around the colon would make the check more robust.

Suggested change
const API_VERSION_DIRECTIVE_RE = /@apiVersion\(version:\s*"([^"]+)"\)/
const API_VERSION_DIRECTIVE_RE = /@apiVersion\(version\s*:\s*"([^"]+)"\)/

Copilot uses AI. Check for mistakes.

async function warnIfSchemaMismatch(extension: ExtensionInstance<FunctionConfigType>) {
const schemaPath = joinPath(extension.directory, 'schema.graphql')
let content: string
try {
content = await readFile(schemaPath)
} catch (error) {
Comment on lines +232 to +237
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check currently reads the entire schema.graphql into memory via readFile(schemaPath). The PR description calls out reading only the first ~512 bytes; doing that would keep the check lightweight even if schemas become large. Consider reading a small prefix (e.g. open + read / or readFile then slice) before applying the regex.

Copilot uses AI. Check for mistakes.
if (error instanceof Error && 'code' in error) return
Copy link

Copilot AI Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The error handler in warnIfSchemaMismatch returns for any filesystem error that happens to have a code property (e.g. EACCES/EPERM), which can hide real problems reading schema.graphql and lead to more cryptic failures later in the build. Only ignore the expected “file not found” case (e.g. code === 'ENOENT') and rethrow other read errors.

Suggested change
if (error instanceof Error && 'code' in error) return
if (error instanceof Error && 'code' in error && error.code === 'ENOENT') return

Copilot uses AI. Check for mistakes.
throw error
}

const match = API_VERSION_DIRECTIVE_RE.exec(content)
if (!match) return

const schemaVersion = match[1]!
const tomlVersion = extension.configuration.api_version
if (schemaVersion !== tomlVersion) {
outputWarn(
`schema.graphql was generated for API version ${schemaVersion} but your extension targets ${tomlVersion}. Run \`shopify app function schema\` to update.`,
)
}
}

async function runCommand(buildCommand: string, extension: ExtensionInstance, options: BuildFunctionExtensionOptions) {
const buildCommandComponents = buildCommand.split(' ')
options.stdout.write(`Building function ${extension.localIdentifier}...`)
Expand Down
Loading