From d2835926b3a2db5632b743f951224691d51e8e1a Mon Sep 17 00:00:00 2001 From: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:26:33 -0700 Subject: [PATCH 1/2] test(nitro): reproduce stale steps after HMR --- packages/core/e2e/dev.test.ts | 63 +++++++++++++++++++++++++++++++++-- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/packages/core/e2e/dev.test.ts b/packages/core/e2e/dev.test.ts index f4750d67cf..a10fcc1c14 100644 --- a/packages/core/e2e/dev.test.ts +++ b/packages/core/e2e/dev.test.ts @@ -1,7 +1,8 @@ import fs from 'node:fs/promises'; import path from 'node:path'; -import { afterEach, beforeAll, describe, expect, test } from 'vitest'; -import { getWorkbenchAppPath } from './utils'; +import { afterEach, assert, beforeAll, describe, expect, test } from 'vitest'; +import { start } from '../src/runtime'; +import { getWorkbenchAppPath, getWorkflowMetadata, setupWorld } from './utils'; export interface DevTestConfig { generatedStepPath: string; @@ -259,6 +260,64 @@ export async function myNewStep() { }); }); + test.runIf(process.env.APP_NAME === 'vite')( + 'should execute updated step logic after HMR', + { timeout: 70_000 }, + async () => { + assert(deploymentUrl); + setupWorld(deploymentUrl); + + const workflowFile = path.join(appPath, workflowsDir, testWorkflowFile); + const content = await fs.readFile(workflowFile, 'utf8'); + const before = 'before HMR'; + const after = 'after HMR'; + const fixture = ` +export async function hmrWorkflow() { + 'use workflow'; + return hmrStep(); +} + +async function hmrStep() { + 'use step'; + return '${before}'; +} +`; + + await fs.writeFile(workflowFile, content + fixture); + restoreFiles.push({ path: workflowFile, content }); + + await pollUntil({ + description: 'generated step output to include the HMR fixture', + check: async () => { + expect(await fs.readFile(generatedStep, 'utf8')).toContain(before); + }, + }); + + const workflow = await getWorkflowMetadata( + deploymentUrl, + `workflows/${testWorkflowFile}`, + 'hmrWorkflow' + ); + const runBefore = await start<[], string>(workflow, []); + expect(await runBefore.returnValue).toBe(before); + + await fs.writeFile( + workflowFile, + (content + fixture).replace(before, after) + ); + + await pollUntil({ + description: 'generated step output to include the HMR update', + check: async () => { + expect(await fs.readFile(generatedStep, 'utf8')).toContain(after); + }, + }); + + const runAfter = await start<[], string>(workflow, []); + expect(await runAfter.returnValue).toBe(after); + } + ); + test( 'should rebuild on adding workflow file', { timeout: 60_000 }, From 401c7f9bfccb529ae8fd91804682231d6b63a14b Mon Sep 17 00:00:00 2001 From: Nathan Colosimo <110621881+NathanColosimo@users.noreply.github.com> Date: Mon, 22 Jun 2026 14:30:21 -0700 Subject: [PATCH 2/2] fix(nitro): reload steps during Vite HMR --- .changeset/fresh-steps-hmr.md | 5 +++++ packages/nitro/src/index.ts | 19 +++++++++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 .changeset/fresh-steps-hmr.md diff --git a/.changeset/fresh-steps-hmr.md b/.changeset/fresh-steps-hmr.md new file mode 100644 index 0000000000..88dfa8248e --- /dev/null +++ b/.changeset/fresh-steps-hmr.md @@ -0,0 +1,5 @@ +--- +'@workflow/nitro': patch +--- + +Reload rebuilt step bundles during Vite development. diff --git a/packages/nitro/src/index.ts b/packages/nitro/src/index.ts index 0b738c9bb0..b068240dbf 100644 --- a/packages/nitro/src/index.ts +++ b/packages/nitro/src/index.ts @@ -1,5 +1,5 @@ -import { createRequire } from 'node:module'; import { mkdirSync, readFileSync, writeFileSync } from 'node:fs'; +import { createRequire } from 'node:module'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { WORKFLOW_QUEUE_TRIGGER } from '@workflow/builders'; import { workflowTransformPlugin } from '@workflow/rollup'; @@ -413,7 +413,13 @@ function addDashboardHandler(nitro: Nitro) { } } -function addVirtualHandler(nitro: Nitro, route: string, buildPath: string) { +type VirtualHandlerPath = 'workflow/webhook.mjs' | 'workflow/workflows.mjs'; + +function addVirtualHandler( + nitro: Nitro, + route: string, + buildPath: VirtualHandlerPath +) { nitro.options.handlers.push({ route, handler: `#${buildPath}`, @@ -421,6 +427,13 @@ function addVirtualHandler(nitro: Nitro, route: string, buildPath: string) { const handlerImportPath = JSON.stringify( join(nitro.options.buildDir, buildPath) ); + const stepsImportPath = JSON.stringify( + join(nitro.options.buildDir, 'workflow/steps.mjs') + ); + const preloadSteps: Record = { + 'workflow/webhook.mjs': '', + 'workflow/workflows.mjs': `await import(/* @vite-ignore */ pathToFileURL(${stepsImportPath}).href + "?t=" + version);`, + }; if (nitro.options.dev) { // Dev mode: load generated workflow bundles from disk at request time. @@ -442,6 +455,7 @@ function addVirtualHandler(nitro: Nitro, route: string, buildPath: string) { if (version !== currentVersion) { currentVersion = version; currentImportPath = pathToFileURL(handlerPath).href + "?t=" + version; + ${preloadSteps[buildPath]} } return (await import(currentImportPath)).POST; } @@ -465,6 +479,7 @@ function addVirtualHandler(nitro: Nitro, route: string, buildPath: string) { if (version !== currentVersion) { currentVersion = version; currentImportPath = pathToFileURL(handlerPath).href + "?t=" + version; + ${preloadSteps[buildPath]} } return (await import(currentImportPath)).POST; }