From f33e03dcecaf3aedfd91f4ada6261ec7c773da56 Mon Sep 17 00:00:00 2001 From: Richard Ward Date: Wed, 31 Dec 2025 13:09:54 +0000 Subject: [PATCH] update: shared,packages,cjs (2 files) --- .../seed/scripts/runtime/script_executor.cjs | 384 ++++++++++++++++++ .../seed/scripts/runtime/test_runner.cjs | 168 ++++++++ 2 files changed, 552 insertions(+) create mode 100644 packages/shared/seed/scripts/runtime/script_executor.cjs create mode 100644 packages/shared/seed/scripts/runtime/test_runner.cjs diff --git a/packages/shared/seed/scripts/runtime/script_executor.cjs b/packages/shared/seed/scripts/runtime/script_executor.cjs new file mode 100644 index 000000000..73c6afdc0 --- /dev/null +++ b/packages/shared/seed/scripts/runtime/script_executor.cjs @@ -0,0 +1,384 @@ +/** + * JSON Script Runtime Executor (CommonJS version) + * Executes script.json definitions at runtime without code generation + */ + +/** + * Reference resolver - resolves $ref:path.to.value + */ +function resolveRef(ref, context) { + if (typeof ref !== 'string' || !ref.startsWith('$ref:')) { + return ref; + } + + const path = ref.substring(5); // Remove "$ref:" + const parts = path.split('.'); + + let value = context; + for (const part of parts) { + if (value === null || typeof value !== 'object') { + return undefined; + } + value = value[part]; + } + + return value; +} + +/** + * Expression evaluator + */ +function evalExpression(expr, context) { + if (typeof expr !== 'object' || expr === null) { + return resolveRef(expr, context); + } + + const exprType = expr.type; + + // Literal values + if (exprType === 'string_literal') return expr.value; + if (exprType === 'number_literal') return expr.value; + if (exprType === 'boolean_literal') return expr.value; + if (exprType === 'null_literal') return null; + + // Template literals + if (exprType === 'template_literal') { + const template = expr.template; + return template.replace(/\$\{([^}]+)\}/g, (match, varPath) => { + const ref = `$ref:local.${varPath}`; + const value = resolveRef(ref, context); + return value !== undefined ? String(value) : ''; + }); + } + + // Binary expressions + if (exprType === 'binary_expression') { + const left = evalExpression(expr.left, context); + const right = evalExpression(expr.right, context); + const op = expr.operator; + + switch (op) { + case '+': return left + right; + case '-': return left - right; + case '*': return left * right; + case '/': return left / right; + case '%': return left % right; + case '==': + case '===': return left === right; + case '!=': + case '!==': return left !== right; + case '<': return left < right; + case '>': return left > right; + case '<=': return left <= right; + case '>=': return left >= right; + default: return undefined; + } + } + + // Logical expressions (short-circuit evaluation) + if (exprType === 'logical_expression') { + const op = expr.operator; + + if (op === '&&' || op === 'and') { + const left = evalExpression(expr.left, context); + if (!left) return left; + return evalExpression(expr.right, context); + } + if (op === '||' || op === 'or') { + const left = evalExpression(expr.left, context); + if (left) return left; + return evalExpression(expr.right, context); + } + if (op === '??') { + const left = evalExpression(expr.left, context); + if (left !== null && left !== undefined) return left; + return evalExpression(expr.right, context); + } + } + + // Unary expressions + if (exprType === 'unary_expression') { + const arg = evalExpression(expr.argument, context); + const op = expr.operator; + + switch (op) { + case '!': + case 'not': return !arg; + case '-': return -arg; + case '+': return +arg; + case '~': return ~arg; + case 'typeof': return typeof arg; + default: return undefined; + } + } + + // Conditional expression (ternary) + if (exprType === 'conditional_expression') { + const test = evalExpression(expr.test, context); + if (test) { + return evalExpression(expr.consequent, context); + } else { + return evalExpression(expr.alternate, context); + } + } + + // Member access + if (exprType === 'member_access') { + const obj = evalExpression(expr.object, context); + if (obj !== null && typeof obj === 'object') { + if (expr.computed) { + const prop = evalExpression(expr.property, context); + return obj[prop]; + } else { + return obj[expr.property]; + } + } + return undefined; + } + + // Call expression + if (exprType === 'call_expression') { + const callee = resolveRef(expr.callee, context); + const args = []; + if (expr.args) { + for (const arg of expr.args) { + args.push(evalExpression(arg, context)); + } + } + + if (typeof callee === 'function') { + return callee(...args); + } + return undefined; + } + + // Object literal + if (exprType === 'object_literal') { + const obj = {}; + if (expr.properties) { + for (const [key, value] of Object.entries(expr.properties)) { + obj[key] = evalExpression(value, context); + } + } + return obj; + } + + // Array literal + if (exprType === 'array_literal') { + const arr = []; + if (expr.elements) { + for (const elem of expr.elements) { + arr.push(evalExpression(elem, context)); + } + } + return arr; + } + + return undefined; +} + +/** + * Statement executor + */ +function executeStatement(stmt, context) { + const stmtType = stmt.type; + + // Variable declarations + if (stmtType === 'const_declaration' || stmtType === 'let_declaration') { + const name = stmt.name; + const value = evalExpression(stmt.value, context); + context.local_vars = context.local_vars || {}; + context.local_vars[name] = value; + return null; + } + + // Assignment + if (stmtType === 'assignment') { + const target = stmt.target; + const value = evalExpression(stmt.value, context); + + if (typeof target === 'string' && target.startsWith('$ref:')) { + const path = target.substring(5); + const parts = path.split('.'); + + if (parts.length === 2 && parts[0] === 'local') { + context.local_vars = context.local_vars || {}; + context.local_vars[parts[1]] = value; + } else if (parts.length === 2 && parts[0] === 'params') { + context.params[parts[1]] = value; + } + } else if (typeof target === 'string') { + context.local_vars = context.local_vars || {}; + context.local_vars[target] = value; + } + return null; + } + + // If statement + if (stmtType === 'if_statement') { + const condition = evalExpression(stmt.condition, context); + if (condition) { + if (stmt.then) { + for (const s of stmt.then) { + const result = executeStatement(s, context); + if (result && result.type === 'return') { + return result; + } + } + } + } else if (stmt.else) { + for (const s of stmt.else) { + const result = executeStatement(s, context); + if (result && result.type === 'return') { + return result; + } + } + } + return null; + } + + // Return statement + if (stmtType === 'return') { + return { + type: 'return', + value: evalExpression(stmt.value, context) + }; + } + + // Try-catch + if (stmtType === 'try_catch') { + try { + if (stmt.try) { + for (const s of stmt.try) { + const result = executeStatement(s, context); + if (result && result.type === 'return') { + return result; + } + } + } + } catch (err) { + if (stmt.catch) { + context.catch = context.catch || {}; + context.catch[stmt.catch.param || 'err'] = err; + + for (const s of stmt.catch.body) { + const result = executeStatement(s, context); + if (result && result.type === 'return') { + return result; + } + } + } + } finally { + if (stmt.finally) { + for (const s of stmt.finally) { + executeStatement(s, context); + } + } + } + + return null; + } + + // Call expression statement + if (stmtType === 'call_expression') { + evalExpression(stmt, context); + return null; + } + + // For-each loop + if (stmtType === 'for_each_loop') { + const iterable = evalExpression(stmt.iterable, context); + if (Array.isArray(iterable)) { + context.local_vars = context.local_vars || {}; + for (const item of iterable) { + context.local_vars[stmt.iterator] = item; + if (stmt.body) { + for (const s of stmt.body) { + const result = executeStatement(s, context); + if (result && result.type === 'return') { + return result; + } + } + } + } + } + return null; + } + + // Comment (ignored at runtime) + if (stmtType === 'comment') { + return null; + } + + return null; +} + +/** + * Execute function from script.json + */ +function executeFunction(scriptJson, functionName, args = []) { + const funcDef = (scriptJson.functions || []).find(fn => fn.name === functionName); + + if (!funcDef) { + throw new Error(`Function not found: ${functionName}`); + } + + const context = { + params: {}, + local_vars: {}, + constants: {}, + imports: {}, + functions: {} + }; + + // Load constants + for (const constant of (scriptJson.constants || [])) { + context.constants[constant.name] = constant.value; + } + + // Set parameters with defaults + for (let i = 0; i < (funcDef.params || []).length; i++) { + const param = funcDef.params[i]; + let value = args[i]; + + if (value === undefined && param.default !== undefined) { + if (typeof param.default === 'string' && param.default.startsWith('$ref:')) { + value = resolveRef(param.default, context); + } else if (typeof param.default === 'object' && param.default !== null) { + value = evalExpression(param.default, context); + } else { + value = param.default; + } + } + + context.params[param.name] = value; + } + + // Execute function body + for (const stmt of (funcDef.body || [])) { + const result = executeStatement(stmt, context); + if (result && result.type === 'return') { + return result.value; + } + } + + return undefined; +} + +/** + * Load and execute synchronously + */ +function loadAndExecuteSync(jsonPath, functionName, args = []) { + const fs = require('fs'); + const content = fs.readFileSync(jsonPath, 'utf-8'); + const scriptJson = JSON.parse(content); + return executeFunction(scriptJson, functionName, args); +} + +module.exports = { + executeFunction, + loadAndExecuteSync, + resolveRef, + evalExpression, + executeStatement +}; diff --git a/packages/shared/seed/scripts/runtime/test_runner.cjs b/packages/shared/seed/scripts/runtime/test_runner.cjs new file mode 100644 index 000000000..f772aaf86 --- /dev/null +++ b/packages/shared/seed/scripts/runtime/test_runner.cjs @@ -0,0 +1,168 @@ +/** + * JSON-based Test Runner for Script Executor (CommonJS version) + * Executes test cases defined in JSON format + */ + +const { executeFunction } = require('./script_executor.cjs'); + +/** + * Deep equality comparison + */ +function deepEqual(a, b) { + if (a === b) return true; + if (a === null || b === null) return false; + if (typeof a !== typeof b) return false; + if (typeof a !== 'object') return false; + + const keysA = Object.keys(a); + const keysB = Object.keys(b); + + if (keysA.length !== keysB.length) return false; + + for (const key of keysA) { + if (!keysB.includes(key)) return false; + if (!deepEqual(a[key], b[key])) return false; + } + + return true; +} + +/** + * Execute a single test case + */ +function executeTestCase(testCase, scriptJson) { + const startTime = Date.now(); + let passed = false; + let actualResult = null; + let error = null; + + try { + actualResult = executeFunction(scriptJson, testCase.function, testCase.args || []); + + // Check result + if (testCase.expected !== undefined) { + passed = deepEqual(actualResult, testCase.expected); + } else if (testCase.expectedError) { + passed = false; // Should have thrown + } else { + passed = true; // No assertion + } + } catch (err) { + error = err; + if (testCase.expectedError) { + // Check if error matches expected + passed = err.message.includes(testCase.expectedError); + } else { + passed = false; + } + } + + const duration = Date.now() - startTime; + + return { + name: testCase.name, + function: testCase.function, + passed, + duration, + actualResult, + expectedResult: testCase.expected, + error: error ? error.message : null + }; +} + +/** + * Execute test suite from JSON + */ +function executeTestSuite(testSuite, scriptJson) { + const results = { + suite: testSuite.name || 'Unnamed Suite', + description: testSuite.description, + total: 0, + passed: 0, + failed: 0, + duration: 0, + tests: [] + }; + + const startTime = Date.now(); + + for (const testCase of (testSuite.tests || [])) { + const result = executeTestCase(testCase, scriptJson); + results.tests.push(result); + results.total++; + if (result.passed) { + results.passed++; + } else { + results.failed++; + } + } + + results.duration = Date.now() - startTime; + + return results; +} + +/** + * Format test results as human-readable string + */ +function formatResults(results) { + const lines = []; + lines.push(`\n${'='.repeat(60)}`); + lines.push(`Test Suite: ${results.suite}`); + if (results.description) { + lines.push(`Description: ${results.description}`); + } + lines.push(`${'='.repeat(60)}\n`); + + for (const test of results.tests) { + const icon = test.passed ? '✅' : '❌'; + lines.push(`${icon} ${test.name}`); + lines.push(` Function: ${test.function}`); + lines.push(` Duration: ${test.duration.toFixed(2)}ms`); + + if (!test.passed) { + lines.push(` Expected: ${JSON.stringify(test.expectedResult)}`); + lines.push(` Actual: ${JSON.stringify(test.actualResult)}`); + if (test.error) { + lines.push(` Error: ${test.error}`); + } + } + lines.push(''); + } + + lines.push(`${'='.repeat(60)}`); + lines.push(`Results: ${results.passed}/${results.total} passed (${results.failed} failed)`); + lines.push(`Total Duration: ${results.duration.toFixed(2)}ms`); + lines.push(`${'='.repeat(60)}\n`); + + if (results.failed === 0) { + lines.push('🎉 All tests passed!'); + } else { + lines.push(`❌ ${results.failed} test(s) failed.`); + } + + return lines.join('\n'); +} + +/** + * Synchronous version for Node.js + */ +function runTestSuiteSync(scriptPath, testPath) { + const fs = require('fs'); + + const scriptContent = fs.readFileSync(scriptPath, 'utf-8'); + const scriptJson = JSON.parse(scriptContent); + + const testContent = fs.readFileSync(testPath, 'utf-8'); + const testSuite = JSON.parse(testContent); + + return executeTestSuite(testSuite, scriptJson); +} + +module.exports = { + executeTestCase, + executeTestSuite, + formatResults, + runTestSuiteSync, + deepEqual +};