Skip to content

Commit e5590c1

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

File tree

3 files changed

+229
-5
lines changed

3 files changed

+229
-5
lines changed

src/methods/run-function.ts

Lines changed: 142 additions & 1 deletion
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
@@ -48,7 +49,7 @@ export async function runFunction(
4849
functionName = path.basename(functionDir);
4950
}
5051

51-
return new Promise((resolve, reject) => {
52+
return new Promise((resolve) => {
5253
const shopifyProcess = spawn('shopify', [
5354
'app', 'function', 'run',
5455
'--export', exportName,
@@ -130,3 +131,143 @@ 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+
* @param {String} [wasmPath] - Optional direct path to the WASM file
140+
* @returns {Object} The function run result
141+
*/
142+
export async function runFunctionWithRunnerDirectly(
143+
exportName: string,
144+
input: Record<string, any>,
145+
functionPath?: string,
146+
wasmPath?: string
147+
): Promise<RunFunctionResult> {
148+
try {
149+
const inputJson = JSON.stringify(input);
150+
151+
let functionDir, functionName;
152+
153+
if (functionPath !== undefined && functionPath !== null) {
154+
// Use provided function path
155+
functionDir = path.resolve(functionPath);
156+
functionName = path.basename(functionDir);
157+
} else {
158+
// Calculate paths correctly for when used as a dependency:
159+
// __dirname = /path/to/function/tests/node_modules/function-testing-helpers/src/methods
160+
// Go up 5 levels to get to function directory: ../../../../../ = /path/to/function
161+
functionDir = path.dirname(path.dirname(path.dirname(path.dirname(path.dirname(__dirname)))));
162+
functionName = path.basename(functionDir);
163+
}
164+
165+
// Find WASM file - use provided path or search common locations
166+
let resolvedWasmPath: string | null = null;
167+
168+
if (wasmPath) {
169+
// Use user-provided WASM path
170+
resolvedWasmPath = path.resolve(wasmPath);
171+
if (!fs.existsSync(resolvedWasmPath)) {
172+
return {
173+
result: null,
174+
error: `WASM file not found at provided path: ${resolvedWasmPath}`
175+
};
176+
}
177+
} else {
178+
// Check Rust build output location
179+
const rustWasmPath = path.join(functionDir, 'target', 'wasm32-wasip1', 'release', `${functionName}.wasm`);
180+
if (fs.existsSync(rustWasmPath)) {
181+
resolvedWasmPath = rustWasmPath;
182+
} else {
183+
// Check dist folder (JavaScript functions)
184+
const distWasmPath = path.join(functionDir, 'dist', 'function.wasm');
185+
if (fs.existsSync(distWasmPath)) {
186+
resolvedWasmPath = distWasmPath;
187+
}
188+
}
189+
190+
if (!resolvedWasmPath) {
191+
return {
192+
result: null,
193+
error: `WASM file not found in function directory: ${functionDir}`
194+
};
195+
}
196+
}
197+
198+
const schemaPath = path.join(functionDir, 'schema.graphql');
199+
const queryPath = path.join(functionDir, 'src', `${exportName}.graphql`);
200+
201+
return new Promise((resolve) => {
202+
const runnerProcess = spawn('function-runner', [
203+
'-f', resolvedWasmPath,
204+
'-e', exportName,
205+
'--json',
206+
'-s', schemaPath,
207+
'-q', queryPath
208+
], {
209+
stdio: ['pipe', 'pipe', 'pipe']
210+
});
211+
212+
let stdout = '';
213+
let stderr = '';
214+
215+
runnerProcess.stdout.on('data', (data) => {
216+
stdout += data.toString();
217+
});
218+
219+
runnerProcess.stderr.on('data', (data) => {
220+
stderr += data.toString();
221+
});
222+
223+
runnerProcess.on('close', (code) => {
224+
if (code !== 0) {
225+
resolve({
226+
result: null,
227+
error: `function-runner failed with exit code ${code}: ${stderr}`
228+
});
229+
return;
230+
}
231+
232+
try {
233+
const result = JSON.parse(stdout);
234+
235+
// function-runner output format: { output: {...} }
236+
resolve({
237+
result: { output: result.output || result },
238+
error: null
239+
});
240+
} catch (parseError) {
241+
resolve({
242+
result: null,
243+
error: `Failed to parse function-runner output: ${parseError instanceof Error ? parseError.message : 'Unknown error'}`
244+
});
245+
}
246+
});
247+
248+
runnerProcess.on('error', (error) => {
249+
resolve({
250+
result: null,
251+
error: `Failed to start function-runner: ${error.message}`
252+
});
253+
});
254+
255+
runnerProcess.stdin.write(inputJson);
256+
runnerProcess.stdin.end();
257+
});
258+
259+
} catch (error) {
260+
if (error instanceof Error) {
261+
return {
262+
result: null,
263+
error: error.message
264+
};
265+
} else {
266+
return {
267+
result: null,
268+
error: 'Unknown error occurred'
269+
};
270+
}
271+
}
272+
}
273+

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: 85 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,93 @@ 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+
54+
expect(result).toBeDefined();
55+
expect(result).toHaveProperty('result');
56+
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+
}
89+
});
90+
91+
it('should work with direct WASM path', async () => {
92+
const exportName = 'cart-validations-generate-run';
93+
const input = {
94+
cart: {
95+
lines: [{ quantity: 1 }]
96+
}
97+
};
98+
const wasmPath = 'test-app/extensions/cart-validation-js/dist/function.wasm';
99+
100+
const result = await runFunctionWithRunnerDirectly(exportName, input, 'test-app/extensions/cart-validation-js', wasmPath);
101+
37102
expect(result).toBeDefined();
38103
expect(result).toHaveProperty('result');
39104
expect(result).toHaveProperty('error');
105+
106+
// If function-runner is available and WASM file exists, should succeed
107+
if (result.error === null) {
108+
expect(result.result).toBeDefined();
109+
expect(result.result).toHaveProperty('output');
110+
}
111+
});
112+
113+
it('should handle invalid WASM path', async () => {
114+
const exportName = 'cart-validations-generate-run';
115+
const input = { cart: { lines: [] } };
116+
const wasmPath = 'nonexistent/path/to/function.wasm';
117+
118+
const result = await runFunctionWithRunnerDirectly(exportName, input, 'test-app/extensions/cart-validation-js', wasmPath);
119+
120+
expect(result).toBeDefined();
121+
expect(result.error).toContain('WASM file not found at provided path');
40122
});
41123
});

0 commit comments

Comments
 (0)