Skip to content

Commit 0a04b23

Browse files
committed
add ability to invoke functionRunner directly
1 parent 3d993d6 commit 0a04b23

File tree

3 files changed

+166
-4
lines changed

3 files changed

+166
-4
lines changed

src/methods/run-function.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import { spawn } from 'child_process';
66
import path from 'path';
77
import { fileURLToPath } from 'url';
8+
import fs from 'fs';
89

910
/**
1011
* Interface for the run function result
@@ -130,3 +131,114 @@ export async function runFunction(
130131
}
131132
}
132133

134+
/**
135+
* Run a function by calling function-runner directly (bypassing Shopify CLI)
136+
* @param {String} exportName - The function export name
137+
* @param {String} input - The input data
138+
* @param {String} [functionPath] - Optional path to the function directory
139+
* @returns {Object} The function run result
140+
*/
141+
export async function runFunctionWithRunnerDirectly(
142+
exportName: string,
143+
input: Record<string, any>,
144+
functionPath?: string
145+
): Promise<RunFunctionResult> {
146+
try {
147+
const inputJson = JSON.stringify(input);
148+
149+
let functionDir;
150+
151+
if (functionPath !== undefined && functionPath !== null) {
152+
// Use provided function path
153+
functionDir = path.resolve(functionPath);
154+
} else {
155+
// Calculate paths correctly for when used as a dependency
156+
functionDir = path.dirname(path.dirname(path.dirname(path.dirname(path.dirname(__dirname)))));
157+
}
158+
159+
// Find the WASM file in target directory
160+
const wasmPath = path.join(functionDir, 'target', 'wasm32-wasip1', 'release', `${path.basename(functionDir)}.wasm`);
161+
const schemaPath = path.join(functionDir, 'schema.graphql');
162+
const queryPath = path.join(functionDir, 'src', `${exportName}.graphql`);
163+
164+
// Verify files exist
165+
if (!fs.existsSync(wasmPath)) {
166+
return {
167+
result: null,
168+
error: `WASM file not found at ${wasmPath}. Make sure the function is built.`
169+
};
170+
}
171+
172+
return new Promise((resolve) => {
173+
const runnerProcess = spawn('function-runner', [
174+
'-f', wasmPath,
175+
'-e', exportName,
176+
'--json',
177+
'-s', schemaPath,
178+
'-q', queryPath
179+
], {
180+
stdio: ['pipe', 'pipe', 'pipe']
181+
});
182+
183+
let stdout = '';
184+
let stderr = '';
185+
186+
runnerProcess.stdout.on('data', (data) => {
187+
stdout += data.toString();
188+
});
189+
190+
runnerProcess.stderr.on('data', (data) => {
191+
stderr += data.toString();
192+
});
193+
194+
runnerProcess.on('close', (code) => {
195+
if (code !== 0) {
196+
resolve({
197+
result: null,
198+
error: `function-runner failed with exit code ${code}: ${stderr}`
199+
});
200+
return;
201+
}
202+
203+
try {
204+
const result = JSON.parse(stdout);
205+
206+
// function-runner output format: { output: {...} }
207+
resolve({
208+
result: { output: result.output || result },
209+
error: null
210+
});
211+
} catch (parseError) {
212+
resolve({
213+
result: null,
214+
error: `Failed to parse function-runner output: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`
215+
});
216+
}
217+
});
218+
219+
runnerProcess.on('error', (error) => {
220+
resolve({
221+
result: null,
222+
error: `Failed to start function-runner: ${error.message}`
223+
});
224+
});
225+
226+
runnerProcess.stdin.write(inputJson);
227+
runnerProcess.stdin.end();
228+
});
229+
230+
} catch (error) {
231+
if (error instanceof Error) {
232+
return {
233+
result: null,
234+
error: error.message
235+
};
236+
} else {
237+
return {
238+
result: null,
239+
error: 'Unknown error occurred'
240+
};
241+
}
242+
}
243+
}
244+

src/wasm-testing-helpers.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { loadFixture } from './methods/load-fixture.js';
1010
import { loadSchema } from './methods/load-schema.js';
1111
import { loadInputQuery } from './methods/load-input-query.js';
1212
import { buildFunction } from './methods/build-function.js';
13-
import { runFunction } from './methods/run-function.js';
13+
import { runFunction, runFunctionWithRunnerDirectly } from './methods/run-function.js';
1414
import { validateTestAssets } from './methods/validate-test-assets.js';
1515
import { validateInputQuery } from './methods/validate-input-query.js';
1616
import { validateFixtureInputStructure } from './methods/validate-fixture-input-structure.js';
@@ -24,6 +24,7 @@ export {
2424
loadInputQuery,
2525
buildFunction,
2626
runFunction,
27+
runFunctionWithRunnerDirectly,
2728
validateTestAssets,
2829
validateInputQuery,
2930
validateFixtureInputStructure,

test/methods/run-function.test.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { describe, it, expect } from 'vitest';
2-
import { runFunction } from '../../src/methods/run-function.ts';
2+
import { runFunction, runFunctionWithRunnerDirectly } from '../../src/methods/run-function.ts';
33
import { loadFixture } from '../../src/methods/load-fixture.ts';
44

55
describe('runFunction', () => {
@@ -31,11 +31,60 @@ describe('runFunction', () => {
3131

3232
it('should work with fixture data', async () => {
3333
const fixture = await loadFixture('test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json');
34-
34+
3535
const result = await runFunction(fixture.export, fixture.input, 'test-app/extensions/cart-validation-js');
36-
36+
37+
expect(result).toBeDefined();
38+
expect(result).toHaveProperty('result');
39+
expect(result).toHaveProperty('error');
40+
});
41+
});
42+
43+
describe('runFunctionWithRunnerDirectly', () => {
44+
it('should run a function using function-runner directly', async () => {
45+
const exportName = 'cart-validations-generate-run';
46+
const input = {
47+
cart: {
48+
lines: [{ quantity: 1 }]
49+
}
50+
};
51+
52+
const result = await runFunctionWithRunnerDirectly(exportName, input, 'test-app/extensions/cart-validation-js');
53+
3754
expect(result).toBeDefined();
3855
expect(result).toHaveProperty('result');
3956
expect(result).toHaveProperty('error');
57+
58+
// If function-runner is available, should succeed
59+
if (result.error === null) {
60+
expect(result.result).toBeDefined();
61+
expect(result.result).toHaveProperty('output');
62+
}
63+
});
64+
65+
it('should handle missing WASM file gracefully', async () => {
66+
const exportName = 'cart-validations-generate-run';
67+
const input = { cart: { lines: [] } };
68+
69+
const result = await runFunctionWithRunnerDirectly(exportName, input, 'nonexistent-function');
70+
71+
expect(result).toBeDefined();
72+
expect(result.error).toContain('WASM file not found');
73+
});
74+
75+
it('should work with fixture data', async () => {
76+
const fixture = await loadFixture('test-app/extensions/cart-validation-js/tests/fixtures/cda6d1.json');
77+
78+
const result = await runFunctionWithRunnerDirectly(fixture.export, fixture.input, 'test-app/extensions/cart-validation-js');
79+
80+
expect(result).toBeDefined();
81+
expect(result).toHaveProperty('result');
82+
expect(result).toHaveProperty('error');
83+
84+
// If function-runner is available and function is built, should succeed
85+
if (result.error === null) {
86+
expect(result.result).toBeDefined();
87+
expect(result.result).toHaveProperty('output');
88+
}
4089
});
4190
});

0 commit comments

Comments
 (0)