From 6f84d6ebc4f8159e40412f8f1aca9dbbc8b11a44 Mon Sep 17 00:00:00 2001 From: Vercel Date: Tue, 11 Nov 2025 03:11:44 +0000 Subject: [PATCH] Fix Windows path normalization in workflow lookup Fixed Windows filesystem support for Vercel Workflow ## Issue Description Users on Windows were experiencing the following error when using workflows: ``` ReferenceError: Workflow "workflow//C:\\dev\\birthday-card-generator\\app\\api\\generate\\route.ts//handleOrder" must be a function, but got "undefined" instead ``` The problem was that workflow names contained Windows path separators (backslashes) during lookup, but workflows were registered with normalized paths (forward slashes) during the SWC transform process. ## Root Cause The SWC plugin correctly normalizes file paths from backslashes to forward slashes when generating workflow IDs during bundling. However, the runtime workflow lookup in `runWorkflow()` was using the original workflow name without normalization. This caused a mismatch between the registered workflow name and the lookup key. ## Solution Implemented Modified the workflow lookup logic in `packages/core/src/workflow.ts` to normalize Windows path separators before attempting to retrieve workflows from the registry: ### Files Modified: 1. **packages/core/src/workflow.ts** - Added path normalization before workflow lookup: `workflowRun.workflowName.replace(/\\/g, '/')` - Updated error message to use the normalized name for consistency 2. **packages/core/src/parse-name.test.ts** - Added test case to verify Windows path normalization works correctly - Test covers the specific error scenario from the bug report 3. **packages/core/src/workflow.test.ts** - Added end-to-end test for Windows path handling in workflow execution - Test simulates the exact scenario where a workflow is registered with forward slashes but looked up with backslashes ## Technical Details The fix ensures that both the workflow registration (via SWC transform) and workflow lookup (via runtime) consistently use forward slash path separators, eliminating the Windows-specific path mismatch issue. ## Testing - All existing tests continue to pass - New tests specifically validate Windows path handling - The fix addresses the exact error scenario reported by the user This change maintains backward compatibility while fixing Windows filesystem support without affecting Unix/Linux systems. Co-authored-by: Vercel --- packages/core/src/parse-name.test.ts | 16 ++++++++++++++++ packages/core/src/workflow.test.ts | 27 +++++++++++++++++++++++++++ packages/core/src/workflow.ts | 9 +++++++-- 3 files changed, 50 insertions(+), 2 deletions(-) diff --git a/packages/core/src/parse-name.test.ts b/packages/core/src/parse-name.test.ts index 236f7cd22..98b45f622 100644 --- a/packages/core/src/parse-name.test.ts +++ b/packages/core/src/parse-name.test.ts @@ -36,6 +36,22 @@ describe('parseWorkflowName', () => { }); }); + test('should handle Windows path with backslashes during workflow name normalization', () => { + // This tests the scenario from the bug report where the workflow name contains backslashes + const windowsWorkflowName = 'workflow//C:\\dev\\birthday-card-generator\\app\\api\\generate\\route.ts//handleOrder'; + const normalizedName = windowsWorkflowName.replace(/\\/g, '/'); + + // The normalized name should equal the expected format + expect(normalizedName).toBe('workflow//C:/dev/birthday-card-generator/app/api/generate/route.ts//handleOrder'); + + const result = parseWorkflowName(normalizedName); + expect(result).toEqual({ + shortName: 'handleOrder', + path: 'C:/dev/birthday-card-generator/app/api/generate/route.ts', + functionName: 'handleOrder', + }); + }); + test('should parse workflow name with nested function names', () => { const result = parseWorkflowName( 'workflow//src/app.ts//nested//function//name' diff --git a/packages/core/src/workflow.test.ts b/packages/core/src/workflow.test.ts index 93d868ede..1771d0662 100644 --- a/packages/core/src/workflow.test.ts +++ b/packages/core/src/workflow.test.ts @@ -2303,5 +2303,32 @@ describe('runWorkflow', () => { 'sleep with date completed' ); }); + + it('should handle Windows paths with backslashes in workflow names', async () => { + // Test the specific Windows path issue from the bug report + const workflowName = 'workflow//C:/dev/birthday-card-generator/app/api/generate/route.ts//handleOrder'; + const workflowCode = `function handleOrder() { return "success from Windows"; }${getWorkflowTransformCode(workflowName)}`; + + // Simulate the problematic workflow name with backslashes (as might occur on Windows) + const windowsWorkflowName = 'workflow//C:\\dev\\birthday-card-generator\\app\\api\\generate\\route.ts//handleOrder'; + + const ops: Promise[] = []; + const workflowRun: WorkflowRun = { + runId: 'wrun_windows_test', + workflowName: windowsWorkflowName, // Use the backslash version + status: 'running', + input: dehydrateWorkflowArguments([], ops), + createdAt: new Date('2024-01-01T00:00:00.000Z'), + updatedAt: new Date('2024-01-01T00:00:00.000Z'), + startedAt: new Date('2024-01-01T00:00:00.000Z'), + deploymentId: 'test-deployment', + }; + + const events: Event[] = []; + + // This should work now because the workflow.ts normalizes the name before lookup + const result = await runWorkflow(workflowCode, workflowRun, events); + expect(result).toBe("success from Windows"); + }); }); }); diff --git a/packages/core/src/workflow.ts b/packages/core/src/workflow.ts index f27f4453f..bf646eb62 100644 --- a/packages/core/src/workflow.ts +++ b/packages/core/src/workflow.ts @@ -549,8 +549,13 @@ export async function runWorkflow( const parsedName = parseWorkflowName(workflowRun.workflowName); const filename = parsedName?.path || workflowRun.workflowName; + // Normalize the workflow name to handle Windows path separators + // The SWC transform normalizes paths to forward slashes during bundling, + // but the workflow name lookup might contain backslashes on Windows + const normalizedWorkflowName = workflowRun.workflowName.replace(/\\/g, '/'); + const workflowFn = runInContext( - `${workflowCode}; globalThis.__private_workflows?.get(${JSON.stringify(workflowRun.workflowName)})`, + `${workflowCode}; globalThis.__private_workflows?.get(${JSON.stringify(normalizedWorkflowName)})`, context, { filename } ); @@ -558,7 +563,7 @@ export async function runWorkflow( if (typeof workflowFn !== 'function') { throw new ReferenceError( `Workflow ${JSON.stringify( - workflowRun.workflowName + normalizedWorkflowName )} must be a function, but got "${typeof workflowFn}" instead` ); }