From b45fc62e7b6614e6d6826b46787a24dec0319aa9 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 00:03:15 -0800 Subject: [PATCH 1/7] fix(codegen): function prologue resolution edge cases --- apps/sim/app/api/function/execute/route.ts | 17 ++++++++++- apps/sim/executor/variables/resolver.ts | 17 +++++++++-- .../sim/executor/variables/resolvers/block.ts | 29 +++++++++++++++---- apps/sim/lib/execution/isolated-vm-worker.cjs | 2 ++ 4 files changed, 56 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 434b2d54d3..8a052bf1c4 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -387,7 +387,12 @@ function resolveWorkflowVariables( if (type === 'number') { variableValue = Number(variableValue) } else if (type === 'boolean') { - variableValue = variableValue === 'true' || variableValue === true + if (typeof variableValue === 'boolean') { + // Already a boolean, keep as-is + } else { + const normalized = String(variableValue).toLowerCase().trim() + variableValue = normalized === 'true' || normalized === '1' + } } else if (type === 'json' && typeof variableValue === 'string') { try { variableValue = JSON.parse(variableValue) @@ -689,6 +694,10 @@ export async function POST(req: NextRequest) { for (const [k, v] of Object.entries(contextVariables)) { if (v === undefined) { prologue += `const ${k} = undefined;\n` + } else if (v === null) { + prologue += `const ${k} = null;\n` + } else if (typeof v === 'boolean' || typeof v === 'number') { + prologue += `const ${k} = ${v};\n` } else { prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n` } @@ -764,6 +773,12 @@ export async function POST(req: NextRequest) { for (const [k, v] of Object.entries(contextVariables)) { if (v === undefined) { prologue += `${k} = None\n` + } else if (v === null) { + prologue += `${k} = None\n` + } else if (typeof v === 'boolean') { + prologue += `${k} = ${v ? 'True' : 'False'}\n` + } else if (typeof v === 'number') { + prologue += `${k} = ${v}\n` } else { prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n` } diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index 980708931b..fcb6307071 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -157,7 +157,14 @@ export class VariableResolver { let replacementError: Error | null = null - // Use generic utility for smart variable reference replacement + const blockType = block?.metadata?.id + const language = + blockType === BlockType.FUNCTION + ? ((block?.config?.params as Record | undefined)?.language as + | string + | undefined) + : undefined + let result = replaceValidReferences(template, (match) => { if (replacementError) return match @@ -167,14 +174,18 @@ export class VariableResolver { return match } - const blockType = block?.metadata?.id const isInTemplateLiteral = blockType === BlockType.FUNCTION && template.includes('${') && template.includes('}') && template.includes('`') - return this.blockResolver.formatValueForBlock(resolved, blockType, isInTemplateLiteral) + return this.blockResolver.formatValueForBlock( + resolved, + blockType, + isInTemplateLiteral, + language + ) } catch (error) { replacementError = error instanceof Error ? error : new Error(String(error)) return match diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index 09d246e80e..d741eadeda 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -162,14 +162,15 @@ export class BlockResolver implements Resolver { public formatValueForBlock( value: any, blockType: string | undefined, - isInTemplateLiteral = false + isInTemplateLiteral = false, + language?: string ): string { if (blockType === 'condition') { return this.stringifyForCondition(value) } if (blockType === 'function') { - return this.formatValueForCodeContext(value, isInTemplateLiteral) + return this.formatValueForCodeContext(value, isInTemplateLiteral, language) } if (blockType === 'response') { @@ -210,7 +211,13 @@ export class BlockResolver implements Resolver { return String(value) } - private formatValueForCodeContext(value: any, isInTemplateLiteral: boolean): string { + private formatValueForCodeContext( + value: any, + isInTemplateLiteral: boolean, + language?: string + ): string { + const isPython = language === 'python' + if (isInTemplateLiteral) { if (typeof value === 'string') { return value @@ -218,6 +225,15 @@ export class BlockResolver implements Resolver { if (typeof value === 'object' && value !== null) { return JSON.stringify(value) } + if (typeof value === 'boolean') { + return isPython ? (value ? 'True' : 'False') : String(value) + } + if (value === undefined) { + return isPython ? 'None' : 'undefined' + } + if (value === null) { + return isPython ? 'None' : 'null' + } return String(value) } @@ -228,10 +244,13 @@ export class BlockResolver implements Resolver { return JSON.stringify(value) } if (value === undefined) { - return 'undefined' + return isPython ? 'None' : 'undefined' } if (value === null) { - return 'null' + return isPython ? 'None' : 'null' + } + if (typeof value === 'boolean') { + return isPython ? (value ? 'True' : 'False') : String(value) } return String(value) } diff --git a/apps/sim/lib/execution/isolated-vm-worker.cjs b/apps/sim/lib/execution/isolated-vm-worker.cjs index f6c587a15f..932ef997df 100644 --- a/apps/sim/lib/execution/isolated-vm-worker.cjs +++ b/apps/sim/lib/execution/isolated-vm-worker.cjs @@ -132,6 +132,8 @@ async function executeCode(request) { for (const [key, value] of Object.entries(contextVariables)) { if (value === undefined) { await jail.set(key, undefined) + } else if (value === null) { + await jail.set(key, null) } else { await jail.set(key, new ivm.ExternalCopy(value).copyInto()) } From 8aabf06f62fd8e5e707d1c587060a9ee88da6352 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 00:09:59 -0800 Subject: [PATCH 2/7] remove hacky fallback --- apps/sim/app/api/function/execute/route.ts | 2 +- apps/sim/lib/workflows/variables/variable-manager.test.ts | 4 ++-- apps/sim/lib/workflows/variables/variable-manager.ts | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 8a052bf1c4..76ae81a8eb 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -391,7 +391,7 @@ function resolveWorkflowVariables( // Already a boolean, keep as-is } else { const normalized = String(variableValue).toLowerCase().trim() - variableValue = normalized === 'true' || normalized === '1' + variableValue = normalized === 'true' } } else if (type === 'json' && typeof variableValue === 'string') { try { diff --git a/apps/sim/lib/workflows/variables/variable-manager.test.ts b/apps/sim/lib/workflows/variables/variable-manager.test.ts index 4796ce19b9..08da0cc85d 100644 --- a/apps/sim/lib/workflows/variables/variable-manager.test.ts +++ b/apps/sim/lib/workflows/variables/variable-manager.test.ts @@ -26,7 +26,7 @@ describe('VariableManager', () => { it.concurrent('should handle boolean type variables', () => { expect(VariableManager.parseInputForStorage('true', 'boolean')).toBe(true) expect(VariableManager.parseInputForStorage('false', 'boolean')).toBe(false) - expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(true) + expect(VariableManager.parseInputForStorage('1', 'boolean')).toBe(false) expect(VariableManager.parseInputForStorage('0', 'boolean')).toBe(false) expect(VariableManager.parseInputForStorage('"true"', 'boolean')).toBe(true) expect(VariableManager.parseInputForStorage("'false'", 'boolean')).toBe(false) @@ -128,7 +128,7 @@ describe('VariableManager', () => { expect(VariableManager.resolveForExecution(false, 'boolean')).toBe(false) expect(VariableManager.resolveForExecution('true', 'boolean')).toBe(true) expect(VariableManager.resolveForExecution('false', 'boolean')).toBe(false) - expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(true) + expect(VariableManager.resolveForExecution('1', 'boolean')).toBe(false) expect(VariableManager.resolveForExecution('0', 'boolean')).toBe(false) }) diff --git a/apps/sim/lib/workflows/variables/variable-manager.ts b/apps/sim/lib/workflows/variables/variable-manager.ts index 04ed5b9e49..7807d466c0 100644 --- a/apps/sim/lib/workflows/variables/variable-manager.ts +++ b/apps/sim/lib/workflows/variables/variable-manager.ts @@ -61,7 +61,7 @@ export class VariableManager { // Special case for 'anything else' in the test if (unquoted === 'anything else') return true const normalized = String(unquoted).toLowerCase().trim() - return normalized === 'true' || normalized === '1' + return normalized === 'true' } case 'object': From 87dcd53b981e30849a5eef816cb8cc2aac949442 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 00:23:33 -0800 Subject: [PATCH 3/7] case insensitive lookup --- apps/sim/executor/utils/block-reference.ts | 25 ++++++++++++++++--- .../executor/variables/resolvers/reference.ts | 23 +++++++++++++++-- 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/apps/sim/executor/utils/block-reference.ts b/apps/sim/executor/utils/block-reference.ts index 590e9d869d..7f0eac85ba 100644 --- a/apps/sim/executor/utils/block-reference.ts +++ b/apps/sim/executor/utils/block-reference.ts @@ -53,17 +53,34 @@ function getProperties(schema: unknown): Record | undefined { : undefined } +function getFieldCaseInsensitive( + obj: Record, + fieldName: string +): unknown | undefined { + if (fieldName in obj) { + return obj[fieldName] + } + const lowerName = fieldName.toLowerCase() + for (const key of Object.keys(obj)) { + if (key.toLowerCase() === lowerName) { + return obj[key] + } + } + return undefined +} + function lookupField(schema: unknown, fieldName: string): unknown | undefined { if (typeof schema !== 'object' || schema === null) return undefined const typed = schema as Record - if (fieldName in typed) { - return typed[fieldName] + const direct = getFieldCaseInsensitive(typed, fieldName) + if (direct !== undefined) { + return direct } const props = getProperties(schema) - if (props && fieldName in props) { - return props[fieldName] + if (props) { + return getFieldCaseInsensitive(props, fieldName) } return undefined diff --git a/apps/sim/executor/variables/resolvers/reference.ts b/apps/sim/executor/variables/resolvers/reference.ts index 9f4b69eec5..7be74cb5b3 100644 --- a/apps/sim/executor/variables/resolvers/reference.ts +++ b/apps/sim/executor/variables/resolvers/reference.ts @@ -20,6 +20,19 @@ export interface Resolver { * navigatePath({a: {b: {c: 1}}}, ['a', 'b', 'c']) => 1 * navigatePath({items: [{name: 'test'}]}, ['items', '0', 'name']) => 'test' */ +function getPropertyCaseInsensitive(obj: Record, key: string): unknown { + if (key in obj) { + return obj[key] + } + const lowerKey = key.toLowerCase() + for (const k of Object.keys(obj)) { + if (k.toLowerCase() === lowerKey) { + return obj[k] + } + } + return undefined +} + export function navigatePath(obj: any, path: string[]): any { let current = obj for (const part of path) { @@ -30,7 +43,10 @@ export function navigatePath(obj: any, path: string[]): any { const arrayMatch = part.match(/^([^[]+)(\[.+)$/) if (arrayMatch) { const [, prop, bracketsPart] = arrayMatch - current = current[prop] + current = + typeof current === 'object' && current !== null + ? getPropertyCaseInsensitive(current, prop) + : undefined if (current === undefined || current === null) { return undefined } @@ -49,7 +65,10 @@ export function navigatePath(obj: any, path: string[]): any { const index = Number.parseInt(part, 10) current = Array.isArray(current) ? current[index] : undefined } else { - current = current[part] + current = + typeof current === 'object' && current !== null + ? getPropertyCaseInsensitive(current, part) + : undefined } } return current From e20ec7ae3c467d11c55c04f646587e11314a8c44 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 00:26:48 -0800 Subject: [PATCH 4/7] fix python nan and inf resolution --- apps/sim/app/api/function/execute/route.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 76ae81a8eb..9e7b9f5731 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -778,7 +778,15 @@ export async function POST(req: NextRequest) { } else if (typeof v === 'boolean') { prologue += `${k} = ${v ? 'True' : 'False'}\n` } else if (typeof v === 'number') { - prologue += `${k} = ${v}\n` + if (Number.isNaN(v)) { + prologue += `${k} = float('nan')\n` + } else if (v === Number.POSITIVE_INFINITY) { + prologue += `${k} = float('inf')\n` + } else if (v === Number.NEGATIVE_INFINITY) { + prologue += `${k} = float('-inf')\n` + } else { + prologue += `${k} = ${v}\n` + } } else { prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n` } From 0e9bcdf1b2dddf6eb1aa3de65f383087542f0109 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 01:10:30 -0800 Subject: [PATCH 5/7] remove template literal check --- apps/sim/executor/utils/block-reference.ts | 25 +++----------- apps/sim/executor/variables/resolver.ts | 13 +------ .../sim/executor/variables/resolvers/block.ts | 34 ++----------------- .../executor/variables/resolvers/reference.ts | 17 ++-------- 4 files changed, 10 insertions(+), 79 deletions(-) diff --git a/apps/sim/executor/utils/block-reference.ts b/apps/sim/executor/utils/block-reference.ts index 7f0eac85ba..590e9d869d 100644 --- a/apps/sim/executor/utils/block-reference.ts +++ b/apps/sim/executor/utils/block-reference.ts @@ -53,34 +53,17 @@ function getProperties(schema: unknown): Record | undefined { : undefined } -function getFieldCaseInsensitive( - obj: Record, - fieldName: string -): unknown | undefined { - if (fieldName in obj) { - return obj[fieldName] - } - const lowerName = fieldName.toLowerCase() - for (const key of Object.keys(obj)) { - if (key.toLowerCase() === lowerName) { - return obj[key] - } - } - return undefined -} - function lookupField(schema: unknown, fieldName: string): unknown | undefined { if (typeof schema !== 'object' || schema === null) return undefined const typed = schema as Record - const direct = getFieldCaseInsensitive(typed, fieldName) - if (direct !== undefined) { - return direct + if (fieldName in typed) { + return typed[fieldName] } const props = getProperties(schema) - if (props) { - return getFieldCaseInsensitive(props, fieldName) + if (props && fieldName in props) { + return props[fieldName] } return undefined diff --git a/apps/sim/executor/variables/resolver.ts b/apps/sim/executor/variables/resolver.ts index fcb6307071..05077c81ec 100644 --- a/apps/sim/executor/variables/resolver.ts +++ b/apps/sim/executor/variables/resolver.ts @@ -174,18 +174,7 @@ export class VariableResolver { return match } - const isInTemplateLiteral = - blockType === BlockType.FUNCTION && - template.includes('${') && - template.includes('}') && - template.includes('`') - - return this.blockResolver.formatValueForBlock( - resolved, - blockType, - isInTemplateLiteral, - language - ) + return this.blockResolver.formatValueForBlock(resolved, blockType, language) } catch (error) { replacementError = error instanceof Error ? error : new Error(String(error)) return match diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index d741eadeda..c952b62021 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -159,18 +159,13 @@ export class BlockResolver implements Resolver { return this.nameToBlockId.get(normalizeName(name)) } - public formatValueForBlock( - value: any, - blockType: string | undefined, - isInTemplateLiteral = false, - language?: string - ): string { + public formatValueForBlock(value: any, blockType: string | undefined, language?: string): string { if (blockType === 'condition') { return this.stringifyForCondition(value) } if (blockType === 'function') { - return this.formatValueForCodeContext(value, isInTemplateLiteral, language) + return this.formatValueForCodeContext(value, language) } if (blockType === 'response') { @@ -211,32 +206,9 @@ export class BlockResolver implements Resolver { return String(value) } - private formatValueForCodeContext( - value: any, - isInTemplateLiteral: boolean, - language?: string - ): string { + private formatValueForCodeContext(value: any, language?: string): string { const isPython = language === 'python' - if (isInTemplateLiteral) { - if (typeof value === 'string') { - return value - } - if (typeof value === 'object' && value !== null) { - return JSON.stringify(value) - } - if (typeof value === 'boolean') { - return isPython ? (value ? 'True' : 'False') : String(value) - } - if (value === undefined) { - return isPython ? 'None' : 'undefined' - } - if (value === null) { - return isPython ? 'None' : 'null' - } - return String(value) - } - if (typeof value === 'string') { return JSON.stringify(value) } diff --git a/apps/sim/executor/variables/resolvers/reference.ts b/apps/sim/executor/variables/resolvers/reference.ts index 7be74cb5b3..2c153154f1 100644 --- a/apps/sim/executor/variables/resolvers/reference.ts +++ b/apps/sim/executor/variables/resolvers/reference.ts @@ -20,19 +20,6 @@ export interface Resolver { * navigatePath({a: {b: {c: 1}}}, ['a', 'b', 'c']) => 1 * navigatePath({items: [{name: 'test'}]}, ['items', '0', 'name']) => 'test' */ -function getPropertyCaseInsensitive(obj: Record, key: string): unknown { - if (key in obj) { - return obj[key] - } - const lowerKey = key.toLowerCase() - for (const k of Object.keys(obj)) { - if (k.toLowerCase() === lowerKey) { - return obj[k] - } - } - return undefined -} - export function navigatePath(obj: any, path: string[]): any { let current = obj for (const part of path) { @@ -45,7 +32,7 @@ export function navigatePath(obj: any, path: string[]): any { const [, prop, bracketsPart] = arrayMatch current = typeof current === 'object' && current !== null - ? getPropertyCaseInsensitive(current, prop) + ? (current as Record)[prop] : undefined if (current === undefined || current === null) { return undefined @@ -67,7 +54,7 @@ export function navigatePath(obj: any, path: string[]): any { } else { current = typeof current === 'object' && current !== null - ? getPropertyCaseInsensitive(current, part) + ? (current as Record)[part] : undefined } } From c26448136146267440037f3c395db7f904835995 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 01:14:07 -0800 Subject: [PATCH 6/7] fix tests --- apps/sim/executor/variables/resolvers/block.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/apps/sim/executor/variables/resolvers/block.test.ts b/apps/sim/executor/variables/resolvers/block.test.ts index f08b22fc27..01a804900f 100644 --- a/apps/sim/executor/variables/resolvers/block.test.ts +++ b/apps/sim/executor/variables/resolvers/block.test.ts @@ -257,15 +257,9 @@ describe('BlockResolver', () => { expect(result).toBe('"hello"') }) - it.concurrent('should format string for function block in template literal', () => { + it.concurrent('should format object for function block', () => { const resolver = new BlockResolver(createTestWorkflow()) - const result = resolver.formatValueForBlock('hello', 'function', true) - expect(result).toBe('hello') - }) - - it.concurrent('should format object for function block in template literal', () => { - const resolver = new BlockResolver(createTestWorkflow()) - const result = resolver.formatValueForBlock({ a: 1 }, 'function', true) + const result = resolver.formatValueForBlock({ a: 1 }, 'function') expect(result).toBe('{"a":1}') }) From 12239a0803dc317f8b8bff258daa19d103254275 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 01:33:06 -0800 Subject: [PATCH 7/7] consolidate literal gen --- apps/sim/app/api/function/execute/route.ts | 31 ++---------- apps/sim/executor/utils/code-formatting.ts | 48 +++++++++++++++++++ .../sim/executor/variables/resolvers/block.ts | 20 +------- 3 files changed, 53 insertions(+), 46 deletions(-) create mode 100644 apps/sim/executor/utils/code-formatting.ts diff --git a/apps/sim/app/api/function/execute/route.ts b/apps/sim/app/api/function/execute/route.ts index 9e7b9f5731..4ccbd8d7c0 100644 --- a/apps/sim/app/api/function/execute/route.ts +++ b/apps/sim/app/api/function/execute/route.ts @@ -8,6 +8,7 @@ import { executeInIsolatedVM } from '@/lib/execution/isolated-vm' import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages' import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants' import { type OutputSchema, resolveBlockReference } from '@/executor/utils/block-reference' +import { formatLiteralForCode } from '@/executor/utils/code-formatting' import { createEnvVarPattern, createWorkflowVariablePattern, @@ -692,15 +693,7 @@ export async function POST(req: NextRequest) { prologue += `const environmentVariables = JSON.parse(${JSON.stringify(JSON.stringify(envVars))});\n` prologueLineCount++ for (const [k, v] of Object.entries(contextVariables)) { - if (v === undefined) { - prologue += `const ${k} = undefined;\n` - } else if (v === null) { - prologue += `const ${k} = null;\n` - } else if (typeof v === 'boolean' || typeof v === 'number') { - prologue += `const ${k} = ${v};\n` - } else { - prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n` - } + prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n` prologueLineCount++ } @@ -771,25 +764,7 @@ export async function POST(req: NextRequest) { prologue += `environmentVariables = json.loads(${JSON.stringify(JSON.stringify(envVars))})\n` prologueLineCount++ for (const [k, v] of Object.entries(contextVariables)) { - if (v === undefined) { - prologue += `${k} = None\n` - } else if (v === null) { - prologue += `${k} = None\n` - } else if (typeof v === 'boolean') { - prologue += `${k} = ${v ? 'True' : 'False'}\n` - } else if (typeof v === 'number') { - if (Number.isNaN(v)) { - prologue += `${k} = float('nan')\n` - } else if (v === Number.POSITIVE_INFINITY) { - prologue += `${k} = float('inf')\n` - } else if (v === Number.NEGATIVE_INFINITY) { - prologue += `${k} = float('-inf')\n` - } else { - prologue += `${k} = ${v}\n` - } - } else { - prologue += `${k} = json.loads(${JSON.stringify(JSON.stringify(v))})\n` - } + prologue += `${k} = ${formatLiteralForCode(v, 'python')}\n` prologueLineCount++ } const wrapped = [ diff --git a/apps/sim/executor/utils/code-formatting.ts b/apps/sim/executor/utils/code-formatting.ts new file mode 100644 index 0000000000..a4a73dee8a --- /dev/null +++ b/apps/sim/executor/utils/code-formatting.ts @@ -0,0 +1,48 @@ +/** + * Formats a JavaScript/TypeScript value as a code literal for the target language. + * Handles special cases like null, undefined, booleans, and Python-specific number representations. + * + * @param value - The value to format + * @param language - Target language ('javascript' or 'python') + * @returns A string literal representation valid in the target language + * + * @example + * formatLiteralForCode(null, 'python') // => 'None' + * formatLiteralForCode(true, 'python') // => 'True' + * formatLiteralForCode(NaN, 'python') // => "float('nan')" + * formatLiteralForCode("hello", 'javascript') // => '"hello"' + * formatLiteralForCode({a: 1}, 'python') // => "json.loads('{\"a\":1}')" + */ +export function formatLiteralForCode(value: unknown, language: 'javascript' | 'python'): string { + const isPython = language === 'python' + + if (value === undefined) { + return isPython ? 'None' : 'undefined' + } + if (value === null) { + return isPython ? 'None' : 'null' + } + if (typeof value === 'boolean') { + return isPython ? (value ? 'True' : 'False') : String(value) + } + if (typeof value === 'number') { + if (Number.isNaN(value)) { + return isPython ? "float('nan')" : 'NaN' + } + if (value === Number.POSITIVE_INFINITY) { + return isPython ? "float('inf')" : 'Infinity' + } + if (value === Number.NEGATIVE_INFINITY) { + return isPython ? "float('-inf')" : '-Infinity' + } + return String(value) + } + if (typeof value === 'string') { + return JSON.stringify(value) + } + // Objects and arrays - Python needs json.loads() because JSON true/false/null aren't valid Python + if (isPython) { + return `json.loads(${JSON.stringify(JSON.stringify(value))})` + } + return JSON.stringify(value) +} diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index c952b62021..63ab361381 100644 --- a/apps/sim/executor/variables/resolvers/block.ts +++ b/apps/sim/executor/variables/resolvers/block.ts @@ -10,6 +10,7 @@ import { type OutputSchema, resolveBlockReference, } from '@/executor/utils/block-reference' +import { formatLiteralForCode } from '@/executor/utils/code-formatting' import { navigatePath, type ResolutionContext, @@ -207,23 +208,6 @@ export class BlockResolver implements Resolver { } private formatValueForCodeContext(value: any, language?: string): string { - const isPython = language === 'python' - - if (typeof value === 'string') { - return JSON.stringify(value) - } - if (typeof value === 'object' && value !== null) { - return JSON.stringify(value) - } - if (value === undefined) { - return isPython ? 'None' : 'undefined' - } - if (value === null) { - return isPython ? 'None' : 'null' - } - if (typeof value === 'boolean') { - return isPython ? (value ? 'True' : 'False') : String(value) - } - return String(value) + return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript') } }