From c7bd48573a60a25f1d2cbadf642f781eb991e1f1 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Mon, 26 Jan 2026 10:16:13 -0800 Subject: [PATCH 01/18] fix(codegen): function prologue resolution edge cases (#3005) * fix(codegen): function prologue resolution edge cases * remove hacky fallback * case insensitive lookup * fix python nan and inf resolution * remove template literal check * fix tests * consolidate literal gen --- apps/sim/app/api/function/execute/route.ts | 20 ++++---- apps/sim/executor/utils/code-formatting.ts | 48 +++++++++++++++++++ apps/sim/executor/variables/resolver.ts | 18 +++---- .../variables/resolvers/block.test.ts | 10 +--- .../sim/executor/variables/resolvers/block.ts | 35 ++------------ .../executor/variables/resolvers/reference.ts | 10 +++- apps/sim/lib/execution/isolated-vm-worker.cjs | 2 + .../variables/variable-manager.test.ts | 4 +- .../workflows/variables/variable-manager.ts | 2 +- 9 files changed, 86 insertions(+), 63 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 434b2d54d..4ccbd8d7c 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, @@ -387,7 +388,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' + } } else if (type === 'json' && typeof variableValue === 'string') { try { variableValue = JSON.parse(variableValue) @@ -687,11 +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 { - prologue += `const ${k} = JSON.parse(${JSON.stringify(JSON.stringify(v))});\n` - } + prologue += `const ${k} = ${formatLiteralForCode(v, 'javascript')};\n` prologueLineCount++ } @@ -762,11 +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 { - 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 000000000..a4a73dee8 --- /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/resolver.ts b/apps/sim/executor/variables/resolver.ts index 980708931..05077c81e 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,7 @@ 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, language) } catch (error) { replacementError = error instanceof Error ? error : new Error(String(error)) return match diff --git a/apps/sim/executor/variables/resolvers/block.test.ts b/apps/sim/executor/variables/resolvers/block.test.ts index f08b22fc2..01a804900 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}') }) diff --git a/apps/sim/executor/variables/resolvers/block.ts b/apps/sim/executor/variables/resolvers/block.ts index 09d246e80..63ab36138 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, @@ -159,17 +160,13 @@ export class BlockResolver implements Resolver { return this.nameToBlockId.get(normalizeName(name)) } - public formatValueForBlock( - value: any, - blockType: string | undefined, - isInTemplateLiteral = false - ): 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) + return this.formatValueForCodeContext(value, language) } if (blockType === 'response') { @@ -210,29 +207,7 @@ export class BlockResolver implements Resolver { return String(value) } - private formatValueForCodeContext(value: any, isInTemplateLiteral: boolean): string { - if (isInTemplateLiteral) { - if (typeof value === 'string') { - return value - } - if (typeof value === 'object' && value !== null) { - return JSON.stringify(value) - } - return String(value) - } - - if (typeof value === 'string') { - return JSON.stringify(value) - } - if (typeof value === 'object' && value !== null) { - return JSON.stringify(value) - } - if (value === undefined) { - return 'undefined' - } - if (value === null) { - return 'null' - } - return String(value) + private formatValueForCodeContext(value: any, language?: string): string { + return formatLiteralForCode(value, language === 'python' ? 'python' : 'javascript') } } diff --git a/apps/sim/executor/variables/resolvers/reference.ts b/apps/sim/executor/variables/resolvers/reference.ts index 9f4b69eec..2c153154f 100644 --- a/apps/sim/executor/variables/resolvers/reference.ts +++ b/apps/sim/executor/variables/resolvers/reference.ts @@ -30,7 +30,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 + ? (current as Record)[prop] + : undefined if (current === undefined || current === null) { return undefined } @@ -49,7 +52,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 + ? (current as Record)[part] + : undefined } } return current diff --git a/apps/sim/lib/execution/isolated-vm-worker.cjs b/apps/sim/lib/execution/isolated-vm-worker.cjs index f6c587a15..932ef997d 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()) } diff --git a/apps/sim/lib/workflows/variables/variable-manager.test.ts b/apps/sim/lib/workflows/variables/variable-manager.test.ts index 4796ce19b..08da0cc85 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 04ed5b9e4..7807d466c 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 56bc809c6fa10b9ea0c8709493f3b85991e77b75 Mon Sep 17 00:00:00 2001 From: Waleed Date: Mon, 26 Jan 2026 13:07:11 -0800 Subject: [PATCH 02/18] fix(docs): separate local and blob asset resolution for quick-reference (#3007) * fix(docs): separate local and blob asset resolution for quick-reference ActionImage now uses local paths directly for PNGs while ActionVideo uses blob storage with proper path normalization (strips static/ prefix). Co-Authored-By: Claude Opus 4.5 * refactor(docs): simplify asset resolution by using correct paths directly Remove path normalization logic from action-media component. Instead, use the appropriate paths in MDX: - PNGs: /static/quick-reference/... (local) - MP4s: quick-reference/... (blob via getAssetUrl) --------- Co-authored-by: Claude Opus 4.5 --- apps/docs/components/ui/action-media.tsx | 6 +-- .../content/docs/en/quick-reference/index.mdx | 38 +++++++++---------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/apps/docs/components/ui/action-media.tsx b/apps/docs/components/ui/action-media.tsx index 13c906368..1f187fb90 100644 --- a/apps/docs/components/ui/action-media.tsx +++ b/apps/docs/components/ui/action-media.tsx @@ -13,11 +13,9 @@ interface ActionVideoProps { } export function ActionImage({ src, alt }: ActionImageProps) { - const resolvedSrc = getAssetUrl(src.startsWith('/') ? src.slice(1) : src) - return ( {alt} @@ -25,7 +23,7 @@ export function ActionImage({ src, alt }: ActionImageProps) { } export function ActionVideo({ src, alt }: ActionVideoProps) { - const resolvedSrc = getAssetUrl(src.startsWith('/') ? src.slice(1) : src) + const resolvedSrc = getAssetUrl(src) return (