Skip to content
Closed
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
6 changes: 6 additions & 0 deletions .changeset/function-prepare-variables-check.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@shopify/app': minor
'@shopify/cli': minor
---

Run `@shopify/shopify_function`'s `check.js`, when the package has it, before building a JavaScript function. It fails the build when a prepare target returns variables its run target's input query doesn't accept.
93 changes: 93 additions & 0 deletions packages/app/src/cli/services/function/build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
runWasmOpt,
runTrampoline,
buildJSFunction,
checkJSFunction,
} from './build.js'
import {
javyBinary,
Expand Down Expand Up @@ -479,6 +480,98 @@ describe('bundleExtension', () => {
})
})

describe('checkJSFunction', () => {
const config = {
name: 'test function',
type: 'function',
build: {wasm_opt: true},
configuration_ui: false,
api_version: '2026-07',
targeting: [
{target: 'cart.validations.generate.prepare', export: 'cart-validations-generate-prepare'},
{
target: 'cart.validations.generate.run',
input_query: 'src/run.graphql',
export: 'cart-validations-generate-run',
},
],
}

async function installCheck(tmpDir: string) {
const check = joinPath(tmpDir, 'node_modules/@shopify/shopify_function/check.js')
await mkdir(dirname(check))
await writeFile(check, '')
return check
}

test("runs @shopify/shopify_function's check.js with the function's targets", async () => {
await inTemporaryDirectory(async (tmpDir) => {
// Given
const ourFunction = await testFunctionExtension({dir: tmpDir, entryPath: 'src/index.js', config})
const check = await installCheck(tmpDir)
let functionJson: unknown
vi.mocked(exec).mockImplementation(async (_command, args) => {
functionJson = JSON.parse(await readFile(args.at(-1)!))
})

// When
await checkJSFunction(ourFunction, {stdout, stderr, signal, app})

// Then
expect(exec).toHaveBeenCalledWith(
process.execPath,
[check, '--function', expect.stringMatching(/function\.json$/)],
{cwd: tmpDir, stderr, signal},
)
expect(functionJson).toEqual({
handle: ourFunction.handle,
directory: tmpDir,
entry: 'src/index.js',
schema: joinPath(tmpDir, 'schema.graphql'),
targets: [
{target: 'cart.validations.generate.prepare', export: 'cartValidationsGeneratePrepare'},
{
target: 'cart.validations.generate.run',
export: 'cartValidationsGenerateRun',
inputQuery: joinPath(tmpDir, 'src/run.graphql'),
},
],
})
})
})

test('does nothing when @shopify/shopify_function has no check.js', async () => {
await inTemporaryDirectory(async (tmpDir) => {
// Given
const ourFunction = await testFunctionExtension({dir: tmpDir, entryPath: 'src/index.js', config})

// When
await checkJSFunction(ourFunction, {stdout, stderr, signal, app})

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

test('fails the build when the check fails', async () => {
await inTemporaryDirectory(async (tmpDir) => {
// Given
const ourFunction = await testFunctionExtension({dir: tmpDir, entryPath: 'src/index.js', config})
await installShopifyLibrary(tmpDir)
const check = await installCheck(tmpDir)
vi.mocked(exec).mockRejectedValueOnce(new Error('prepare variables mismatch'))

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

// Then
await expect(got).rejects.toThrow('prepare variables mismatch')
expect(exec).toHaveBeenCalledTimes(1)
expect(exec).toHaveBeenCalledWith(process.execPath, [check, '--function', expect.any(String)], expect.anything())
})
})
})

describe('runJavy', () => {
test('captures compiler diagnostics in the dev session log in text mode', async () => {
const {exec: execute} =
Expand Down
31 changes: 31 additions & 0 deletions packages/app/src/cli/services/function/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export async function buildJSFunction(fun: ExtensionInstance<FunctionConfigType>
const javyBuilder: JavyBuilder = exports.length === 0 ? DefaultJavyBuilder : new ExportJavyBuilder(exports)

const deps = await validateShopifyFunctionPackageVersion(fun)
await checkJSFunction(fun, options)
Comment thread
saga-dasgupta marked this conversation as resolved.
Resolved

if (options.useTasks) {
return buildJSFunctionWithTasks(fun, options, javyBuilder, deps)
Expand All @@ -102,6 +103,36 @@ export async function buildJSFunction(fun: ExtensionInstance<FunctionConfigType>
}
}

// Runs @shopify/shopify_function's check.js, when the package has it, before the function is built.
export async function checkJSFunction(fun: ExtensionInstance<FunctionConfigType>, options: JSFunctionBuildOptions) {
const check = await findPathUp('node_modules/@shopify/shopify_function/check.js', {type: 'file', cwd: fun.directory})
if (!check) return

await inTemporaryDirectory(async (temporaryDirectory) => {
// Each target's export is the function the bundle imports for it
const functionPath = joinPath(temporaryDirectory, 'function.json')
await writeFile(
functionPath,
JSON.stringify({
handle: fun.handle,
directory: fun.directory,
entry: fun.entrySourceFilePath,
schema: joinPath(fun.directory, 'schema.graphql'),
targets: (fun.configuration.targeting ?? []).map((targeting) => ({
target: targeting.target,
export: targeting.export && camelize(targeting.export),
inputQuery: targeting.input_query && joinPath(fun.directory, targeting.input_query),
})),
}),
)
await exec(process.execPath, [check, '--function', functionPath], {
cwd: fun.directory,
stderr: options.stderr,
signal: options.signal,
})
})
}

async function buildJSFunctionWithoutTasks(
fun: ExtensionInstance<FunctionConfigType>,
options: JSFunctionBuildOptions,
Expand Down
Loading