Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
87ce599
Proper stacktrace propogation in world
pranaygp Nov 9, 2025
e532869
Standardize the error type in the world spec
pranaygp Nov 10, 2025
cd1888b
Normalize Workbenches
pranaygp Nov 10, 2025
ce120f2
fix error tests post normalization
pranaygp Nov 10, 2025
67fb677
fix(sveltekit): reading file on hmr delete
adriandlam Nov 10, 2025
2563978
changeset
adriandlam Nov 10, 2025
ea58dc1
fix(vite): add resolve symlink script
adriandlam Nov 10, 2025
63d92ce
fix(vite): missing building on hmr
adriandlam Nov 10, 2025
a59f764
test local builder in vite
adriandlam Nov 10, 2025
63a9c4c
test: increase timeout on hookWorkflow
adriandlam Nov 10, 2025
761278f
test: ignore vite based apps in crossFileWorkflow
adriandlam Nov 10, 2025
f30c295
test: fix nitro based apps status codes
adriandlam Nov 10, 2025
4959f80
fix: intercept default vite spa handler on 404 workflow routes
adriandlam Nov 11, 2025
6f92685
fix: vite hook route returning 422
adriandlam Nov 11, 2025
f94a38b
test: use 422 for hookWorkflow expected
adriandlam Nov 11, 2025
e2ea115
test: fix hono returning 404
adriandlam Nov 11, 2025
482d0da
chore: add comment to middleware to clarify
adriandlam Nov 11, 2025
b181a03
make api route for duplicate case
adriandlam Nov 11, 2025
353d877
revert
adriandlam Nov 11, 2025
06b916d
revert: nitro builder
adriandlam Nov 11, 2025
12a524d
add back nitro unhandled rejection logic
adriandlam Nov 11, 2025
00f3e51
test: add hono
adriandlam Nov 11, 2025
4792d39
changeset
adriandlam Nov 11, 2025
ef98862
fix: unused method
adriandlam Nov 12, 2025
47e7bfb
fix: remove duplicate import
adriandlam Nov 12, 2025
a38c472
remove
adriandlam Nov 12, 2025
6b5d113
chore: add comments to clarify
adriandlam Nov 12, 2025
b6d2d7c
test remove vite symlink script
adriandlam Nov 12, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/eager-lands-rhyme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/nitro": patch
---

Add Vite middleware to handle 404s in workflow routes from Nitro and silence undefined unhandled rejections
5 changes: 5 additions & 0 deletions .changeset/five-planets-push.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/sveltekit": patch
---

Fix SvelteKit plugin reading deleted files on HMR
2 changes: 2 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,8 @@ jobs:
project-id: "prj_oTgiz3SGX2fpZuM6E0P38Ts8de6d"
- name: "sveltekit"
project-id: "prj_MqnBLm71ceXGSnm3Fs8i8gBnI23G"
- name: "hono"
project-id: "prj_p0GIEsfl53L7IwVbosPvi9rPSOYW"
env:
TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }}
TURBO_TEAM: ${{ vars.TURBO_TEAM }}
Expand Down
15 changes: 10 additions & 5 deletions packages/core/e2e/e2e.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@
])('addTenWorkflow', { timeout: 60_000 }, async (workflow) => {
const run = await triggerWorkflow(workflow, [123]);
const returnValue = await getWorkflowReturnValue(run.runId);
expect(returnValue).toBe(133);

Check failure on line 82 in packages/core/e2e/e2e.test.ts

View workflow job for this annotation

GitHub Actions / E2E Local Dev Tests (hono - stable)

packages/core/e2e/e2e.test.ts > e2e > addTenWorkflow

AssertionError: expected { error: true, …(4) } to be 133 // Object.is equality - Expected: 133 + Received: { "error": true, "message": "fetch failed", "stack": [ "fetch failed", "at node:internal/deps/undici/undici:14900:13", ], "status": 500, "url": "http://localhost:3000/api/trigger?runId=wrun_01K9WHCTP9Y9J6NPFMGGJ4T0VE", } ❯ packages/core/e2e/e2e.test.ts:82:25

const { json } = await cliInspectJson(`runs ${run.runId} --withData`);
expect(json).toMatchObject({
Expand Down Expand Up @@ -155,7 +155,10 @@
method: 'POST',
body: JSON.stringify({ token: 'invalid' }),
});
expect(res.status).toBe(404);
// NOTE: For Nitro apps (Vite, Hono, etc.) in dev mode, status 404 does some
// unexpected stuff and could return a Vite SPA fallback or can cause a Hono route to hang.
// This is because Nitro passes the 404 requests to the dev server to handle.
expect(res.status).toBeOneOf([404, 422]);
body = await res.json();
expect(body).toBeNull();

Expand Down Expand Up @@ -579,14 +582,16 @@
expect(returnValue.cause).toHaveProperty('stack');
expect(typeof returnValue.cause.stack).toBe('string');

// Known issue: SvelteKit dev mode has incorrect source map mappings for bundled imports.
// Known issue: vite-based frameworks dev mode has incorrect source map mappings for bundled imports.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

crazy

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @pi0 you know about this?

// esbuild with bundle:true inlines helpers.ts but source maps incorrectly map to 99_e2e.ts
// This works correctly in production and other frameworks.
// TODO: Investigate esbuild source map generation for bundled modules
const isSvelteKitDevMode =
process.env.APP_NAME === 'sveltekit' && isLocalDeployment();
const isViteBasedFrameworkDevMode =
(process.env.APP_NAME === 'sveltekit' ||
process.env.APP_NAME === 'vite') &&
isLocalDeployment();

if (!isSvelteKitDevMode) {
if (!isViteBasedFrameworkDevMode) {
// Stack trace should include frames from the helper module (helpers.ts)
expect(returnValue.cause.stack).toContain('helpers.ts');
}
Expand Down
13 changes: 12 additions & 1 deletion packages/nitro/src/builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,21 @@ export class LocalBuilder extends BaseBuilder {
inputFiles,
});

const webhookRouteFile = join(this.#outDir, 'webhook.mjs');

await this.createWebhookBundle({
outfile: join(this.#outDir, 'webhook.mjs'),
outfile: webhookRouteFile,
bundle: false,
});

// Post-process the generated file to wrap with SvelteKit request converter
let webhookRouteContent = await readFile(webhookRouteFile, 'utf-8');

// NOTE: This is a workaround to avoid crashing in local dev when context isn't set for waitUntil()
webhookRouteContent = `process.on('unhandledRejection', (reason) => { if (reason !== undefined) console.error('Unhandled rejection detected', reason); });
${webhookRouteContent}`;

await writeFile(webhookRouteFile, webhookRouteContent);
}
}

Expand Down
92 changes: 91 additions & 1 deletion packages/nitro/src/vite.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import type { Nitro } from 'nitro/types';
import type { Plugin } from 'vite';
import type { HotUpdateOptions, Plugin } from 'vite';
import { LocalBuilder } from './builders.js';
import type { ModuleOptions } from './index.js';
import nitroModule from './index.js';
import { workflowRollupPlugin } from './rollup.js';

export function workflow(options?: ModuleOptions): Plugin[] {
let builder: LocalBuilder | undefined;

return [
workflowRollupPlugin(),
{
Expand All @@ -18,9 +21,96 @@ export function workflow(options?: ModuleOptions): Plugin[] {
...options,
_vite: true,
};
if (nitro.options.dev) {
builder = new LocalBuilder(nitro);
}
return nitroModule.setup(nitro);
},
},
// NOTE: This is a workaround because Nitro passes the 404 requests to the dev server to handle.
// For workflow routes, we override to send an empty body to prevent Hono/Vite's SPA fallback.
configureServer(server) {
// Add middleware to intercept 404s on workflow routes before Vite's SPA fallback
return () => {
server.middlewares.use((req, res, next) => {
// Only handle workflow webhook routes
if (!req.url?.startsWith('/.well-known/workflow/v1/')) {
return next();
}

// Wrap writeHead to ensure we send empty body for 404s
const originalWriteHead = res.writeHead;
res.writeHead = function (this: typeof res, ...args: any[]) {
const statusCode = typeof args[0] === 'number' ? args[0] : 200;

// NOTE: Workaround because Nitro passes 404 requests to the vite to handle.
// Causes `webhook route with invalid token` test to fail.
// For 404s on workflow routes, ensure we're sending the right headers
if (statusCode === 404) {
// Set content-length to 0 to prevent Vite from overriding
res.setHeader('Content-Length', '0');
}

// @ts-expect-error - Complex overload signature
return originalWriteHead.apply(this, args);
} as any;

next();
});
};
},
// TODO: Move this to @workflow/vite or something since this is vite specific
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can start this by extracting it to a new function. Vite plugin can return an array (so reusable parts can be shared between nitro/svelete)

async hotUpdate(options: HotUpdateOptions) {
const { file, server, read } = options;

// Check if this is a TS/JS file that might contain workflow directives
const jsTsRegex = /\.(ts|tsx|js|jsx|mjs|cjs)$/;
if (!jsTsRegex.test(file)) {
return;
}

// Read the file to check for workflow/step directives
let content: string;
try {
content = await read();
} catch {
// File might have been deleted - trigger rebuild to update generated routes
console.log('Workflow file deleted, rebuilding...');
if (builder) {
await builder.build();
}
// NOTE: Might be too aggressive
server.ws.send({
type: 'full-reload',
path: '*',
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm too aggressive?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

probably fine for now and we can revisit

});
return;
}

const useWorkflowPattern = /^\s*(['"])use workflow\1;?\s*$/m;
const useStepPattern = /^\s*(['"])use step\1;?\s*$/m;

if (
!useWorkflowPattern.test(content) &&
!useStepPattern.test(content)
) {
return;
}

// Trigger full reload - this will cause Nitro's dev:reload hook to fire,
// which will rebuild workflows and update routes
console.log('Workflow file changed, rebuilding...');
if (builder) {
await builder.build();
}
server.ws.send({
type: 'full-reload',
path: '*',
});

// Let Vite handle the normal HMR for the changed file
return;
},
},
];
}
26 changes: 24 additions & 2 deletions packages/sveltekit/src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,22 @@ export function workflowPlugin(options?: WorkflowPluginOptions): Plugin {
}

// Read the file to check for workflow/step directives
const content = await read();
let content: string;
try {
content = await read();
} catch {
// File might have been deleted - trigger rebuild to update generated routes
console.log('Workflow file deleted, regenerating routes...');
try {
await builder.build();
} catch (buildError) {
// Build might fail if files are being deleted during test cleanup
// Log but don't crash - the next successful change will trigger a rebuild
console.error('Build failed during file deletion:', buildError);
}
return;
}

const useWorkflowPattern = /^\s*(['"])use workflow\1;?\s*$/m;
const useStepPattern = /^\s*(['"])use step\1;?\s*$/m;

Expand All @@ -123,7 +138,14 @@ export function workflowPlugin(options?: WorkflowPluginOptions): Plugin {

// Rebuild everything - simpler and more reliable than tracking individual files
console.log('Workflow file changed, regenerating routes...');
await builder.build();
try {
await builder.build();
} catch (buildError) {
// Build might fail if files are being modified/deleted during test cleanup
// Log but don't crash - the next successful change will trigger a rebuild
console.error('Build failed during HMR:', buildError);
return;
}

// Trigger full reload of workflow routes
server.ws.send({
Expand Down
4 changes: 2 additions & 2 deletions workbench/example/api/trigger.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getRun, start } from 'workflow/api';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import workflowManifest from '../manifest.js';
import {
WorkflowRunFailedError,
WorkflowRunNotCompletedError,
} from 'workflow/internal/errors';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import workflowManifest from '../manifest.js';

export async function POST(req: Request) {
const url = new URL(req.url);
Expand Down
2 changes: 1 addition & 1 deletion workbench/example/workflows/7_full.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { sleep, createWebhook } from 'workflow';
import { createWebhook, sleep } from 'workflow';

export async function handleUserSignup(email: string) {
'use workflow';
Expand Down
9 changes: 5 additions & 4 deletions workbench/hono/server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { Hono } from 'hono';
import { getHookByToken, getRun, resumeHook, start } from 'workflow/api';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import { allWorkflows } from './_workflows.js';
import {
WorkflowRunFailedError,
WorkflowRunNotCompletedError,
} from 'workflow/internal/errors';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import { allWorkflows } from './_workflows.js';

const app = new Hono();

Expand Down Expand Up @@ -163,8 +163,9 @@ app.post('/api/hook', async ({ req }) => {
} catch (error) {
console.log('error during getHookByToken', error);
// TODO: `WorkflowAPIError` is not exported, so for now
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cc @TooTallNate we should have a HookNotFound error thrown from the world?

// we'll return 404 assuming it's the "invalid" token test case
return Response.json(null, { status: 404 });
// we'll return 422 assuming it's the "invalid" token test case
// NOTE: Need to return 422 because Nitro passes 404 requests to the dev server to handle.
return Response.json(null, { status: 422 });
}

await resumeHook(hook.token, {
Expand Down
4 changes: 2 additions & 2 deletions workbench/nextjs-turbopack/app/api/trigger/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getRun, start } from 'workflow/api';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import { allWorkflows } from '@/_workflows';
import {
WorkflowRunFailedError,
WorkflowRunNotCompletedError,
} from 'workflow/internal/errors';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import { allWorkflows } from '@/_workflows';

export async function POST(req: Request) {
const url = new URL(req.url);
Expand Down
11 changes: 11 additions & 0 deletions workbench/nextjs-webpack/app/api/duplicate-case/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// NOTE: This route isn't needed/ever used, we're just
// using it because webpack relies on esbuild's tree shaking

import { start } from 'workflow/api';
import { addTenWorkflow } from '@/workflows/98_duplicate_case';

export async function GET(_: Request) {
const run = await start(addTenWorkflow, [10]);
const result = await run.returnValue;
return Response.json({ result });
}
4 changes: 2 additions & 2 deletions workbench/nextjs-webpack/app/api/trigger/route.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { getRun, start } from 'workflow/api';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import { allWorkflows } from '@/_workflows';
import {
WorkflowRunFailedError,
WorkflowRunNotCompletedError,
} from 'workflow/internal/errors';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import { allWorkflows } from '@/_workflows';

export async function POST(req: Request) {
const url = new URL(req.url);
Expand Down
6 changes: 3 additions & 3 deletions workbench/sveltekit/src/routes/api/trigger/+server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import { type RequestHandler } from '@sveltejs/kit';
import type { RequestHandler } from '@sveltejs/kit';
import { getRun, start } from 'workflow/api';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import { allWorkflows } from '$lib/_workflows.js';
import {
WorkflowRunFailedError,
WorkflowRunNotCompletedError,
} from 'workflow/internal/errors';
import { hydrateWorkflowArguments } from 'workflow/internal/serialization';
import { allWorkflows } from '$lib/_workflows.js';

export const POST: RequestHandler = async ({ request }) => {
const url = new URL(request.url);
Expand Down
5 changes: 3 additions & 2 deletions workbench/vite/routes/api/hook.post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ export default async ({ req }: { req: Request }) => {
} catch (error) {
console.log('error during getHookByToken', error);
// TODO: `WorkflowAPIError` is not exported, so for now
// we'll return 404 assuming it's the "invalid" token test case
return Response.json(null, { status: 404 });
// we'll return 422 assuming it's the "invalid" token test case
// NOTE: Need to return 422 because Nitro passes 404 requests to the dev server to handle.
return Response.json(null, { status: 422 });
}

await resumeHook(hook.token, {
Expand Down
2 changes: 1 addition & 1 deletion workbench/vite/vite.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { defineConfig } from 'vite';
import { nitro } from 'nitro/vite';
import { defineConfig } from 'vite';
import { workflow } from 'workflow/vite';

export default defineConfig({
Expand Down
Loading