diff --git a/.changeset/eager-lands-rhyme.md b/.changeset/eager-lands-rhyme.md new file mode 100644 index 0000000000..8bab4c8b61 --- /dev/null +++ b/.changeset/eager-lands-rhyme.md @@ -0,0 +1,5 @@ +--- +"@workflow/nitro": patch +--- + +Add Vite middleware to handle 404s in workflow routes from Nitro and silence undefined unhandled rejections diff --git a/.changeset/five-planets-push.md b/.changeset/five-planets-push.md new file mode 100644 index 0000000000..cf4965fa27 --- /dev/null +++ b/.changeset/five-planets-push.md @@ -0,0 +1,5 @@ +--- +"@workflow/sveltekit": patch +--- + +Fix SvelteKit plugin reading deleted files on HMR diff --git a/.claude/commands/demo.md b/.claude/commands/demo.md new file mode 100644 index 0000000000..487dd22bc1 --- /dev/null +++ b/.claude/commands/demo.md @@ -0,0 +1,9 @@ +--- +description: Run the 7_full demo workflow +allowed-tools: Bash(curl:*), Bash(npx workflow:*), Bash(pnpm dev) +--- + + +Start the $ARUGMENTS workbench (default to the nextjs turboback workbench available in the workbenches directory). Run it in dev mode, and also start the workflow web UI (run `npx workflow web` inside the appropriate workbench directory). + +Then trigger the 7_full.ts workflow example. you can see how to trigger a specific example by looking at the trigger API route for the workbench - it is probably just a POST request using bash (maybe curl) to this endpoint: > diff --git a/.claude/settings.json b/.claude/settings.json index 0a5f2bdab2..9b3e777270 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -8,7 +8,7 @@ "Bash(pnpm build:*)", "Bash(pnpm typecheck:*)" ], - "deny": ["Bash(curl:*)", "Read(./.env)", "Read(./.env.*)"], + "deny": ["Read(./.env)", "Read(./.env.*)"], "additionalDirectories": ["../workflow-server"] } } diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 534ad8790a..ff4f9d27cd 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -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 }} @@ -181,10 +183,7 @@ jobs: run: pnpm turbo run build --filter='!./workbench/*' - name: Resolve symlinks - run: | - if [ -f "workbench/${{ matrix.app.name }}/resolve-symlinks.sh" ]; then - cd workbench/${{ matrix.app.name }} && ./resolve-symlinks.sh - fi + run: ./scripts/resolve-symlinks.sh workbench/${{ matrix.app.name }} - name: Run E2E Tests run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/dev.test.ts && pnpm run test:e2e diff --git a/packages/core/e2e/dev.test.ts b/packages/core/e2e/dev.test.ts index 578a012881..04052c0408 100644 --- a/packages/core/e2e/dev.test.ts +++ b/packages/core/e2e/dev.test.ts @@ -10,6 +10,8 @@ export interface DevTestConfig { apiFileImportPath: string; /** The workflow file to modify for testing HMR. Defaults to '3_streams.ts' */ testWorkflowFile?: string; + /** The workflows directory relative to appPath. Defaults to 'workflows' */ + workflowsDir?: string; } function getConfigFromEnv(): DevTestConfig | null { @@ -39,6 +41,7 @@ export function createDevTests(config?: DevTestConfig) { finalConfig.generatedWorkflowPath ); const testWorkflowFile = finalConfig.testWorkflowFile ?? '3_streams.ts'; + const workflowsDir = finalConfig.workflowsDir ?? 'workflows'; const restoreFiles: Array<{ path: string; content: string }> = []; afterEach(async () => { @@ -55,7 +58,7 @@ export function createDevTests(config?: DevTestConfig) { }); test('should rebuild on workflow change', { timeout: 10_000 }, async () => { - const workflowFile = path.join(appPath, 'workflows', testWorkflowFile); + const workflowFile = path.join(appPath, workflowsDir, testWorkflowFile); const content = await fs.readFile(workflowFile, 'utf8'); @@ -83,7 +86,7 @@ export async function myNewWorkflow() { }); test('should rebuild on step change', { timeout: 10_000 }, async () => { - const stepFile = path.join(appPath, 'workflows', testWorkflowFile); + const stepFile = path.join(appPath, workflowsDir, testWorkflowFile); const content = await fs.readFile(stepFile, 'utf8'); @@ -114,7 +117,11 @@ export async function myNewStep() { 'should rebuild on adding workflow file', { timeout: 10_000 }, async () => { - const workflowFile = path.join(appPath, 'workflows', 'new-workflow.ts'); + const workflowFile = path.join( + appPath, + workflowsDir, + 'new-workflow.ts' + ); await fs.writeFile( workflowFile, @@ -132,7 +139,7 @@ export async function myNewStep() { await fs.writeFile( apiFile, - `import '${finalConfig.apiFileImportPath}/workflows/new-workflow'; + `import '${finalConfig.apiFileImportPath}/${workflowsDir}/new-workflow'; ${apiFileContent}` ); diff --git a/packages/core/e2e/e2e.test.ts b/packages/core/e2e/e2e.test.ts index 006aa64eae..5b6ce1aa69 100644 --- a/packages/core/e2e/e2e.test.ts +++ b/packages/core/e2e/e2e.test.ts @@ -90,10 +90,11 @@ describe('e2e', () => { output: 133, }); // In local vs. vercel backends, the workflow name is different, so we check for either, - // since this test runs against both. + // since this test runs against both. Also different workbenches have different directory structures. expect(json.workflowName).toBeOneOf([ `workflow//example/${workflow.workflowFile}//${workflow.workflowFn}`, `workflow//${workflow.workflowFile}//${workflow.workflowFn}`, + `workflow//src/${workflow.workflowFile}//${workflow.workflowFn}`, ]); }); @@ -154,7 +155,10 @@ describe('e2e', () => { 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(); @@ -578,14 +582,16 @@ describe('e2e', () => { 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. // 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'); } diff --git a/packages/core/e2e/local-build.test.ts b/packages/core/e2e/local-build.test.ts index 119201c881..512beb425f 100644 --- a/packages/core/e2e/local-build.test.ts +++ b/packages/core/e2e/local-build.test.ts @@ -12,6 +12,7 @@ describe.each([ 'vite', 'sveltekit', 'nuxt', + 'hono', ])('e2e', (project) => { test('builds without errors', { timeout: 180_000 }, async () => { // skip if we're targeting specific app to test diff --git a/packages/nitro/src/builders.ts b/packages/nitro/src/builders.ts index e9944be1de..ea9f26cfee 100644 --- a/packages/nitro/src/builders.ts +++ b/packages/nitro/src/builders.ts @@ -63,19 +63,25 @@ 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, + suppressUndefinedRejections: true, }); } } export function getWorkflowDirs(nitro: Nitro) { + const srcDir = nitro.options.srcDir || nitro.options.rootDir; + return unique( [ ...(nitro.options.workflow?.dirs ?? []), - join(nitro.options.rootDir, 'workflows'), - ...nitro.options.scanDirs.map((dir) => join(dir, 'workflows')), + join(srcDir, 'workflows'), + join(srcDir, nitro.options.routesDir || 'routes'), + join(srcDir, nitro.options.apiDir || 'api'), ].map((dir) => resolve(nitro.options.rootDir, dir)) ).sort(); } diff --git a/packages/nitro/src/index.ts b/packages/nitro/src/index.ts index 74f5e2021f..60a2dd5fc8 100644 --- a/packages/nitro/src/index.ts +++ b/packages/nitro/src/index.ts @@ -91,7 +91,14 @@ function addVirtualHandler(nitro: Nitro, route: string, buildPath: string) { // Nitro v3+ (native web handlers) nitro.options.virtual[`#${buildPath}`] = /* js */ ` import { POST } from "${join(nitro.options.buildDir, buildPath)}"; - export default ({ req }) => POST(req); + export default async ({ req }) => { + try { + return await POST(req); + } catch (error) { + console.error('Handler error:', error); + return new Response('Internal Server Error', { status: 500 }); + } + }; `; } } diff --git a/packages/nitro/src/vite.ts b/packages/nitro/src/vite.ts index 74b9233786..f210250227 100644 --- a/packages/nitro/src/vite.ts +++ b/packages/nitro/src/vite.ts @@ -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(), { @@ -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 + 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: '*', + }); + 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; + }, }, ]; } diff --git a/packages/nitro/test/dirs.test.ts b/packages/nitro/test/dirs.test.ts index 233f86b8a0..b50270e8ed 100644 --- a/packages/nitro/test/dirs.test.ts +++ b/packages/nitro/test/dirs.test.ts @@ -15,7 +15,7 @@ const nitroMock = (dirs: string[]) => { describe('nitro:getWorkflowDirs', () => { test('default dirs', () => { const result = getWorkflowDirs(nitroMock([])); - expect(result).toEqual(['/root/server/workflows', '/root/workflows']); + expect(result).toEqual(['/root/api', '/root/routes', '/root/workflows']); }); test('custom dirs', () => { @@ -24,8 +24,9 @@ describe('nitro:getWorkflowDirs', () => { ); expect(result).toEqual([ '/custom/dir2', + '/root/api', '/root/relative/dir1', - '/root/server/workflows', + '/root/routes', '/root/workflows', ]); }); diff --git a/packages/sveltekit/src/builder.ts b/packages/sveltekit/src/builder.ts index 07d3d56045..e26b568c5b 100644 --- a/packages/sveltekit/src/builder.ts +++ b/packages/sveltekit/src/builder.ts @@ -19,14 +19,16 @@ async function convertSvelteKitRequest(request) { export class SvelteKitBuilder extends BaseBuilder { constructor(config?: Partial) { + const workingDir = config?.workingDir || process.cwd(); + super({ ...config, - dirs: ['workflows'], + dirs: ['workflows', 'src/workflows', 'routes', 'src/routes'], buildTarget: 'sveltekit' as const, stepsBundlePath: '', // unused in base workflowsBundlePath: '', // unused in base webhookBundlePath: '', // unused in base - workingDir: config?.workingDir || process.cwd(), + workingDir, }); } diff --git a/packages/sveltekit/src/plugin.ts b/packages/sveltekit/src/plugin.ts index 308d75d37a..0041bddee3 100644 --- a/packages/sveltekit/src/plugin.ts +++ b/packages/sveltekit/src/plugin.ts @@ -103,7 +103,22 @@ export function workflowPlugin(): 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; @@ -113,7 +128,14 @@ export function workflowPlugin(): 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({ diff --git a/scripts/create-test-matrix.mjs b/scripts/create-test-matrix.mjs index dee883a2c0..3fb213d93f 100644 --- a/scripts/create-test-matrix.mjs +++ b/scripts/create-test-matrix.mjs @@ -29,12 +29,19 @@ const DEV_TEST_CONFIGS = { generatedWorkflowPath: 'src/routes/.well-known/workflow/v1/flow/+server.js', apiFilePath: 'src/routes/api/chat/+server.ts', apiFileImportPath: '../../../..', + workflowsDir: 'src/workflows', }, vite: { - generatedStepPath: 'dist/workflow/steps.mjs', - generatedWorkflowPath: 'dist/workflow/workflows.mjs', - apiFilePath: 'src/main.ts', - apiFileImportPath: '..', + generatedStepPath: '.nitro/workflow/steps.mjs', + generatedWorkflowPath: '.nitro/workflow/workflows.mjs', + apiFilePath: 'routes/api/trigger.post.ts', + apiFileImportPath: '../..', + }, + hono: { + generatedStepPath: '.nitro/workflow/steps.mjs', + generatedWorkflowPath: '.nitro/workflow/workflows.mjs', + apiFilePath: 'server.ts', + apiFileImportPath: '.', }, }; @@ -81,4 +88,16 @@ matrix.app.push({ ...DEV_TEST_CONFIGS.nuxt, }); +matrix.app.push({ + name: 'hono', + project: 'workbench-hono-workflow', + ...DEV_TEST_CONFIGS.hono, +}); + +matrix.app.push({ + name: 'vite', + project: 'workbench-vite-workflow', + ...DEV_TEST_CONFIGS.vite, +}); + console.log(JSON.stringify(matrix)); diff --git a/workbench/nextjs-webpack/resolve-symlinks.sh b/scripts/resolve-symlinks.sh similarity index 77% rename from workbench/nextjs-webpack/resolve-symlinks.sh rename to scripts/resolve-symlinks.sh index 6c8a7acbbb..317ed1fe33 100755 --- a/workbench/nextjs-webpack/resolve-symlinks.sh +++ b/scripts/resolve-symlinks.sh @@ -1,8 +1,26 @@ #!/bin/bash set -e -# Script to recursively resolve all symlinks in the app directory -# This is needed for CI where Next.js dev mode doesn't work well with symlinks +# Script to recursively resolve all symlinks in a workbench directory +# This is needed for CI where dev mode doesn't work well with symlinks +# +# Usage: ./scripts/resolve-symlinks.sh +# Example: ./scripts/resolve-symlinks.sh workbench/nextjs-turbopack + +# Check if directory argument is provided +if [ -z "$1" ]; then + echo "Error: No directory specified" + echo "Usage: $0 " + exit 1 +fi + +TARGET_DIR="$1" + +# Check if directory exists +if [ ! -d "$TARGET_DIR" ]; then + echo "Error: Directory '$TARGET_DIR' does not exist" + exit 1 +fi # Only run in CI if [ -z "$CI" ]; then @@ -11,6 +29,9 @@ if [ -z "$CI" ]; then exit 1 fi +echo "Resolving symlinks in $TARGET_DIR..." +cd "$TARGET_DIR" + echo "Resolving symlinked files in workflows directory..." # Special handling for workflows directory if it's a symlink @@ -84,4 +105,4 @@ git ls-files -z --cached --others --exclude-standard | xargs -0 -I {} sh -c 'tes fi done -echo "All symlinks resolved successfully!" +echo "All symlinks resolved successfully in $TARGET_DIR!" diff --git a/workbench/example/api/trigger.ts b/workbench/example/api/trigger.ts index bd6ae39b0d..aa7e79f032 100644 --- a/workbench/example/api/trigger.ts +++ b/workbench/example/api/trigger.ts @@ -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); diff --git a/workbench/sveltekit/workflows/user-signup.ts b/workbench/example/workflows/7_full.ts similarity index 100% rename from workbench/sveltekit/workflows/user-signup.ts rename to workbench/example/workflows/7_full.ts diff --git a/workbench/hono/.gitignore b/workbench/hono/.gitignore index 178daca81d..c80a833d3b 100644 --- a/workbench/hono/.gitignore +++ b/workbench/hono/.gitignore @@ -4,3 +4,4 @@ manifest.js .output .data .vercel +_workflows.ts diff --git a/workbench/hono/_workflows.ts b/workbench/hono/_workflows.ts deleted file mode 120000 index 217286881e..0000000000 --- a/workbench/hono/_workflows.ts +++ /dev/null @@ -1 +0,0 @@ -../nitro-v3/_workflows.ts \ No newline at end of file diff --git a/workbench/hono/nitro.config.ts b/workbench/hono/nitro.config.ts deleted file mode 120000 index 26adc6aeaa..0000000000 --- a/workbench/hono/nitro.config.ts +++ /dev/null @@ -1 +0,0 @@ -../nitro-v3/nitro.config.ts \ No newline at end of file diff --git a/workbench/hono/nitro.config.ts b/workbench/hono/nitro.config.ts new file mode 100644 index 0000000000..4123a1f61a --- /dev/null +++ b/workbench/hono/nitro.config.ts @@ -0,0 +1,11 @@ +import { defineNitroConfig } from 'nitro/config'; + +export default defineNitroConfig({ + modules: ['workflow/nitro'], + handlers: [ + { + route: '/api/**', + handler: './server.ts', + }, + ], +}); diff --git a/workbench/hono/package.json b/workbench/hono/package.json index b8d93c4c76..9e247c5bf0 100644 --- a/workbench/hono/package.json +++ b/workbench/hono/package.json @@ -5,8 +5,12 @@ "version": "0.0.0", "license": "Apache-2.0", "scripts": { + "generate:workflows": "node ../scripts/generate-workflows-registry.js", + "predev": "pnpm generate:workflows", + "prebuild": "pnpm generate:workflows", "dev": "nitro dev", - "build": "nitro build" + "build": "nitro build", + "start": "node .output/server/index.mjs" }, "devDependencies": { "workflow": "workspace:*", diff --git a/workbench/hono/server.ts b/workbench/hono/server.ts index 9d46255bca..d4509cc27e 100644 --- a/workbench/hono/server.ts +++ b/workbench/hono/server.ts @@ -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(); @@ -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 - // 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, { @@ -176,4 +177,24 @@ app.post('/api/hook', async ({ req }) => { return Response.json(hook); }); -export default app; +app.post('/api/test-direct-step-call', async ({ req }) => { + // This route tests calling step functions directly outside of any workflow context + // After the SWC compiler changes, step functions in client mode have their directive removed + // and keep their original implementation, allowing them to be called as regular async functions + const { add } = await import('./workflows/99_e2e.js'); + + const body = await req.json(); + const { x, y } = body; + + console.log(`Calling step function directly with x=${x}, y=${y}`); + + // Call step function directly as a regular async function (no workflow context) + const result = await add(x, y); + console.log(`add(${x}, ${y}) = ${result}`); + + return Response.json({ result }); +}); + +export default async (event: { req: Request }) => { + return app.fetch(event.req); +}; diff --git a/workbench/nextjs-turbopack/.gitignore b/workbench/nextjs-turbopack/.gitignore index e3a7542e06..16abee95e3 100644 --- a/workbench/nextjs-turbopack/.gitignore +++ b/workbench/nextjs-turbopack/.gitignore @@ -40,3 +40,6 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts .env*.local + +# workflow +_workflows.ts diff --git a/workbench/nextjs-turbopack/app/api/trigger/route.ts b/workbench/nextjs-turbopack/app/api/trigger/route.ts index 71767e52f1..f9b8d5ef41 100644 --- a/workbench/nextjs-turbopack/app/api/trigger/route.ts +++ b/workbench/nextjs-turbopack/app/api/trigger/route.ts @@ -1,20 +1,37 @@ import { getRun, start } from 'workflow/api'; -import { hydrateWorkflowArguments } from 'workflow/internal/serialization'; -import * as batchingWorkflow from '@/workflows/6_batching'; -import * as duplicateE2e from '@/workflows/98_duplicate_case'; -import * as e2eWorkflows from '@/workflows/99_e2e'; 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); const workflowFile = url.searchParams.get('workflowFile') || 'workflows/99_e2e.ts'; - const workflowFn = url.searchParams.get('workflowFn') || 'simple'; + if (!workflowFile) { + return new Response('No workflowFile query parameter provided', { + status: 400, + }); + } + const workflows = allWorkflows[workflowFile as keyof typeof allWorkflows]; + if (!workflows) { + return new Response(`Workflow file "${workflowFile}" not found`, { + status: 400, + }); + } - console.log('calling workflow', { workflowFile, workflowFn }); + const workflowFn = url.searchParams.get('workflowFn') || 'simple'; + if (!workflowFn) { + return new Response('No workflow query parameter provided', { + status: 400, + }); + } + const workflow = workflows[workflowFn as keyof typeof workflows]; + if (!workflow) { + return new Response(`Workflow "${workflowFn}" not found`, { status: 400 }); + } let args: any[] = []; @@ -34,21 +51,10 @@ export async function POST(req: Request) { args = [42]; } } - console.log( - `Starting "${workflowFile}/${workflowFn}" workflow with args: ${args}` - ); + console.log(`Starting "${workflowFn}" workflow with args: ${args}`); try { - let workflows; - if (workflowFile === 'workflows/99_e2e.ts') { - workflows = e2eWorkflows; - } else if (workflowFile === 'workflows/6_batching.ts') { - workflows = batchingWorkflow; - } else { - workflows = duplicateE2e; - } - - const run = await start((workflows as any)[workflowFn], args); + const run = await start(workflow as any, args as any); console.log('Run:', run); return Response.json(run); } catch (err) { diff --git a/workbench/nextjs-turbopack/package.json b/workbench/nextjs-turbopack/package.json index 935ced8abd..b111579b54 100644 --- a/workbench/nextjs-turbopack/package.json +++ b/workbench/nextjs-turbopack/package.json @@ -4,9 +4,12 @@ "private": true, "license": "Apache-2.0", "scripts": { + "generate:workflows": "node ../scripts/generate-workflows-registry.js", + "predev": "pnpm generate:workflows", + "prebuild": "pnpm generate:workflows", "dev": "next dev --turbopack", "build": "next build --turbopack", - "clean": "rm -rf .next .swc app/.well-known/workflow", + "clean": "rm -rf .next .swc app/.well-known/workflow _workflows.ts", "start": "next start", "lint": "next lint" }, diff --git a/workbench/nextjs-turbopack/resolve-symlinks.sh b/workbench/nextjs-turbopack/resolve-symlinks.sh deleted file mode 100755 index 425bd7296e..0000000000 --- a/workbench/nextjs-turbopack/resolve-symlinks.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -e - -# Script to recursively resolve all symlinks in the app directory -# This is needed for CI where Next.js dev mode doesn't work well with symlinks - -# Only run in CI -if [ -z "$CI" ]; then - echo "Error: This script should only be run in CI environments" - echo "If you need to resolve symlinks locally, run it manually with CI=true" - exit 1 -fi - -echo "Resolving all symlinks in current directory..." - -# Find all symlinks in current directory (including nested ones), excluding gitignored files -git ls-files -z --cached --others --exclude-standard | xargs -0 -I {} sh -c 'test -L "{}" && echo "{}"' | while read -r symlink; do - # Get the target of the symlink - target=$(readlink "$symlink") - - # Check if target is absolute or relative - if [[ "$target" = /* ]]; then - resolved_target="$target" - else - # Resolve relative symlink path - symlink_dir=$(dirname "$symlink") - resolved_target="$symlink_dir/$target" - fi - - echo "Resolving: $symlink -> $resolved_target" - - # Remove the symlink - rm "$symlink" - - # Copy the target to the symlink location - if [ -d "$resolved_target" ]; then - cp -r "$resolved_target" "$symlink" - else - cp "$resolved_target" "$symlink" - fi -done - -echo "All symlinks resolved successfully!" diff --git a/workbench/nextjs-turbopack/workflows/1_simple.ts b/workbench/nextjs-turbopack/workflows/1_simple.ts new file mode 120000 index 0000000000..32386ef043 --- /dev/null +++ b/workbench/nextjs-turbopack/workflows/1_simple.ts @@ -0,0 +1 @@ +../../example/workflows/1_simple.ts \ No newline at end of file diff --git a/workbench/nextjs-turbopack/workflows/7_full.ts b/workbench/nextjs-turbopack/workflows/7_full.ts new file mode 120000 index 0000000000..660fd8736e --- /dev/null +++ b/workbench/nextjs-turbopack/workflows/7_full.ts @@ -0,0 +1 @@ +../../example/workflows/7_full.ts \ No newline at end of file diff --git a/workbench/nextjs-webpack/.gitignore b/workbench/nextjs-webpack/.gitignore index e3a7542e06..16abee95e3 100644 --- a/workbench/nextjs-webpack/.gitignore +++ b/workbench/nextjs-webpack/.gitignore @@ -40,3 +40,6 @@ yarn-error.log* *.tsbuildinfo next-env.d.ts .env*.local + +# workflow +_workflows.ts diff --git a/workbench/nextjs-webpack/app/api b/workbench/nextjs-webpack/app/api deleted file mode 120000 index 65ccfb8e40..0000000000 --- a/workbench/nextjs-webpack/app/api +++ /dev/null @@ -1 +0,0 @@ -../../nextjs-turbopack/app/api \ No newline at end of file diff --git a/workbench/nextjs-webpack/app/api/chat/route.ts b/workbench/nextjs-webpack/app/api/chat/route.ts new file mode 100644 index 0000000000..da18db04db --- /dev/null +++ b/workbench/nextjs-webpack/app/api/chat/route.ts @@ -0,0 +1,8 @@ +// THIS FILE IS JUST FOR TESTING HMR AS AN ENTRY NEEDS +// TO IMPORT THE WORKFLOWS TO DISCOVER THEM AND WATCH +import * as workflows from '@/workflows/3_streams'; + +export async function POST(_req: Request) { + console.log(workflows); + return Response.json('hello world'); +} diff --git a/workbench/nextjs-webpack/app/api/duplicate-case/route.ts b/workbench/nextjs-webpack/app/api/duplicate-case/route.ts new file mode 100644 index 0000000000..b30a7e1f57 --- /dev/null +++ b/workbench/nextjs-webpack/app/api/duplicate-case/route.ts @@ -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 }); +} diff --git a/workbench/nextjs-webpack/app/api/hook/route.ts b/workbench/nextjs-webpack/app/api/hook/route.ts new file mode 100644 index 0000000000..4a28822c67 --- /dev/null +++ b/workbench/nextjs-webpack/app/api/hook/route.ts @@ -0,0 +1,24 @@ +import { getHookByToken, resumeHook } from 'workflow/api'; + +export const POST = async (request: Request) => { + const { token, data } = await request.json(); + + let hook: Awaited>; + try { + hook = await getHookByToken(token); + console.log('hook', hook); + } 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 }); + } + + await resumeHook(hook.token, { + ...data, + // @ts-expect-error metadata is not typed + customData: hook.metadata?.customData, + }); + + return Response.json(hook); +}; diff --git a/workbench/nextjs-webpack/app/api/test-direct-step-call/route.ts b/workbench/nextjs-webpack/app/api/test-direct-step-call/route.ts new file mode 100644 index 0000000000..5c3e8decc9 --- /dev/null +++ b/workbench/nextjs-webpack/app/api/test-direct-step-call/route.ts @@ -0,0 +1,18 @@ +// This route tests calling step functions directly outside of any workflow context +// After the SWC compiler changes, step functions in client mode have their directive removed +// and keep their original implementation, allowing them to be called as regular async functions + +import { add } from '@/workflows/99_e2e'; + +export async function POST(req: Request) { + const body = await req.json(); + const { x, y } = body; + + console.log(`Calling step function directly with x=${x}, y=${y}`); + + // Call step function directly as a regular async function (no workflow context) + const result = await add(x, y); + console.log(`add(${x}, ${y}) = ${result}`); + + return Response.json({ result }); +} diff --git a/workbench/nextjs-webpack/app/api/trigger/route.ts b/workbench/nextjs-webpack/app/api/trigger/route.ts new file mode 100644 index 0000000000..d1dafb427e --- /dev/null +++ b/workbench/nextjs-webpack/app/api/trigger/route.ts @@ -0,0 +1,149 @@ +import { getRun, start } from 'workflow/api'; +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); + const workflowFile = + url.searchParams.get('workflowFile') || 'workflows/99_e2e.ts'; + const workflowFn = url.searchParams.get('workflowFn') || 'simple'; + + console.log('calling workflow', { workflowFile, workflowFn }); + + let args: any[] = []; + + // Args from query string + const argsParam = url.searchParams.get('args'); + if (argsParam) { + args = argsParam.split(',').map((arg) => { + const num = parseFloat(arg); + return Number.isNaN(num) ? arg.trim() : num; + }); + } else { + // Args from body + const body = await req.text(); + if (body) { + args = hydrateWorkflowArguments(JSON.parse(body), globalThis); + } else { + args = [42]; + } + } + console.log( + `Starting "${workflowFile}/${workflowFn}" workflow with args: ${args}` + ); + + try { + const workflows = allWorkflows[workflowFile as keyof typeof allWorkflows]; + if (!workflows) { + return Response.json( + { error: `Workflow file "${workflowFile}" not found` }, + { status: 404 } + ); + } + + const workflow = workflows[workflowFn as keyof typeof workflows]; + if (!workflow) { + return Response.json( + { error: `Function "${workflowFn}" not found in ${workflowFile}` }, + { status: 400 } + ); + } + + const run = await start(workflow as any, args); + console.log('Run:', run); + return Response.json(run); + } catch (err) { + console.error(`Failed to start!!`, err); + throw err; + } +} + +export async function GET(req: Request) { + const url = new URL(req.url); + const runId = url.searchParams.get('runId'); + if (!runId) { + return new Response('No runId provided', { status: 400 }); + } + + const outputStreamParam = url.searchParams.get('output-stream'); + if (outputStreamParam) { + const namespace = outputStreamParam === '1' ? undefined : outputStreamParam; + const run = getRun(runId); + const stream = run.getReadable({ + namespace, + }); + // Add JSON framing to the stream, wrapping binary data in base64 + const streamWithFraming = new TransformStream({ + transform(chunk, controller) { + const data = + chunk instanceof Uint8Array + ? { data: Buffer.from(chunk).toString('base64') } + : chunk; + controller.enqueue(`${JSON.stringify(data)}\n`); + }, + }); + return new Response(stream.pipeThrough(streamWithFraming), { + headers: { + 'Content-Type': 'application/octet-stream', + }, + }); + } + + try { + const run = getRun(runId); + const returnValue = await run.returnValue; + console.log('Return value:', returnValue); + return returnValue instanceof ReadableStream + ? new Response(returnValue, { + headers: { + 'Content-Type': 'application/octet-stream', + }, + }) + : Response.json(returnValue); + } catch (error) { + if (error instanceof Error) { + if (WorkflowRunNotCompletedError.is(error)) { + return Response.json( + { + ...error, + name: error.name, + message: error.message, + }, + { status: 202 } + ); + } + + if (WorkflowRunFailedError.is(error)) { + const cause = error.cause; + return Response.json( + { + ...error, + name: error.name, + message: error.message, + cause: { + message: cause.message, + stack: cause.stack, + code: cause.code, + }, + }, + { status: 400 } + ); + } + } + + console.error( + 'Unexpected error while getting workflow return value:', + error + ); + return Response.json( + { + error: 'Internal server error', + }, + { status: 500 } + ); + } +} diff --git a/workbench/nextjs-webpack/package.json b/workbench/nextjs-webpack/package.json index 2a7b0fba1a..51a41d20a2 100644 --- a/workbench/nextjs-webpack/package.json +++ b/workbench/nextjs-webpack/package.json @@ -4,9 +4,12 @@ "private": true, "license": "Apache-2.0", "scripts": { + "generate:workflows": "node ../scripts/generate-workflows-registry.js", + "predev": "pnpm generate:workflows", + "prebuild": "pnpm generate:workflows", "dev": "next dev --webpack", "build": "next build --webpack", - "clean": "rm -rf .next .swc app/.well-known/workflow", + "clean": "rm -rf .next .swc app/.well-known/workflow _workflows.ts", "start": "next start", "lint": "next lint" }, diff --git a/workbench/nextjs-webpack/workflows b/workbench/nextjs-webpack/workflows deleted file mode 120000 index ca7d3e96d3..0000000000 --- a/workbench/nextjs-webpack/workflows +++ /dev/null @@ -1 +0,0 @@ -../nextjs-turbopack/workflows \ No newline at end of file diff --git a/workbench/nextjs-webpack/workflows/1_simple.ts b/workbench/nextjs-webpack/workflows/1_simple.ts new file mode 120000 index 0000000000..32386ef043 --- /dev/null +++ b/workbench/nextjs-webpack/workflows/1_simple.ts @@ -0,0 +1 @@ +../../example/workflows/1_simple.ts \ No newline at end of file diff --git a/workbench/sveltekit/workflows/3_streams.ts b/workbench/nextjs-webpack/workflows/3_streams.ts similarity index 100% rename from workbench/sveltekit/workflows/3_streams.ts rename to workbench/nextjs-webpack/workflows/3_streams.ts diff --git a/workbench/sveltekit/workflows/6_batching.ts b/workbench/nextjs-webpack/workflows/6_batching.ts similarity index 100% rename from workbench/sveltekit/workflows/6_batching.ts rename to workbench/nextjs-webpack/workflows/6_batching.ts diff --git a/workbench/nextjs-webpack/workflows/7_full.ts b/workbench/nextjs-webpack/workflows/7_full.ts new file mode 120000 index 0000000000..660fd8736e --- /dev/null +++ b/workbench/nextjs-webpack/workflows/7_full.ts @@ -0,0 +1 @@ +../../example/workflows/7_full.ts \ No newline at end of file diff --git a/workbench/sveltekit/workflows/98_duplicate_case.ts b/workbench/nextjs-webpack/workflows/98_duplicate_case.ts similarity index 100% rename from workbench/sveltekit/workflows/98_duplicate_case.ts rename to workbench/nextjs-webpack/workflows/98_duplicate_case.ts diff --git a/workbench/sveltekit/workflows/99_e2e.ts b/workbench/nextjs-webpack/workflows/99_e2e.ts similarity index 100% rename from workbench/sveltekit/workflows/99_e2e.ts rename to workbench/nextjs-webpack/workflows/99_e2e.ts diff --git a/workbench/sveltekit/workflows/helpers.ts b/workbench/nextjs-webpack/workflows/helpers.ts similarity index 100% rename from workbench/sveltekit/workflows/helpers.ts rename to workbench/nextjs-webpack/workflows/helpers.ts diff --git a/workbench/nitro-v2/.gitignore b/workbench/nitro-v2/.gitignore index 178daca81d..c80a833d3b 100644 --- a/workbench/nitro-v2/.gitignore +++ b/workbench/nitro-v2/.gitignore @@ -4,3 +4,4 @@ manifest.js .output .data .vercel +_workflows.ts diff --git a/workbench/nitro-v2/package.json b/workbench/nitro-v2/package.json index 92fdcf0040..c2c2609171 100644 --- a/workbench/nitro-v2/package.json +++ b/workbench/nitro-v2/package.json @@ -5,8 +5,12 @@ "version": "0.0.0", "license": "Apache-2.0", "scripts": { + "generate:workflows": "node ../scripts/generate-workflows-registry.js", + "predev": "pnpm generate:workflows", + "prebuild": "pnpm generate:workflows", "dev": "nitro dev", - "build": "nitro build" + "build": "nitro build", + "start": "node .output/server/index.mjs" }, "devDependencies": { "@types/node": "catalog:", diff --git a/workbench/nitro-v2/server/_workflows.ts b/workbench/nitro-v2/server/_workflows.ts deleted file mode 120000 index defbb2204c..0000000000 --- a/workbench/nitro-v2/server/_workflows.ts +++ /dev/null @@ -1 +0,0 @@ -../../nitro-v3/_workflows.ts \ No newline at end of file diff --git a/workbench/nitro-v3/.gitignore b/workbench/nitro-v3/.gitignore index 178daca81d..c80a833d3b 100644 --- a/workbench/nitro-v3/.gitignore +++ b/workbench/nitro-v3/.gitignore @@ -4,3 +4,4 @@ manifest.js .output .data .vercel +_workflows.ts diff --git a/workbench/nitro-v3/_workflows.ts b/workbench/nitro-v3/_workflows.ts deleted file mode 100644 index a7ef65eb33..0000000000 --- a/workbench/nitro-v3/_workflows.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as demo from './workflows/0_demo.js'; -import * as simple from './workflows/1_simple.js'; -import * as controlFlow from './workflows/2_control_flow.js'; -import * as streams from './workflows/3_streams.js'; -import * as ai from './workflows/4_ai.js'; -import * as hooks from './workflows/5_hooks.js'; -import * as batching from './workflows/6_batching.js'; -import * as duplicate from './workflows/98_duplicate_case.js'; -import * as e2e from './workflows/99_e2e.js'; - -export const allWorkflows = { - 'workflows/0_calc.ts': demo, - 'workflows/1_simple.ts': simple, - 'workflows/2_control_flow.ts': controlFlow, - 'workflows/3_streams.ts': streams, - 'workflows/4_ai.ts': ai, - 'workflows/5_hooks.ts': hooks, - 'workflows/6_batching.ts': batching, - 'workflows/98_duplicate_case.ts': duplicate, - 'workflows/99_e2e.ts': e2e, -}; diff --git a/workbench/nitro-v3/package.json b/workbench/nitro-v3/package.json index bf388b6df3..1c72f68755 100644 --- a/workbench/nitro-v3/package.json +++ b/workbench/nitro-v3/package.json @@ -5,6 +5,9 @@ "version": "0.0.0", "license": "Apache-2.0", "scripts": { + "generate:workflows": "node ../scripts/generate-workflows-registry.js", + "predev": "pnpm generate:workflows", + "prebuild": "pnpm generate:workflows", "dev": "nitro dev", "build": "nitro build", "start": "node .output/server/index.mjs" diff --git a/workbench/nitro-v3/resolve-symlinks.sh b/workbench/nitro-v3/resolve-symlinks.sh deleted file mode 100755 index 425bd7296e..0000000000 --- a/workbench/nitro-v3/resolve-symlinks.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -e - -# Script to recursively resolve all symlinks in the app directory -# This is needed for CI where Next.js dev mode doesn't work well with symlinks - -# Only run in CI -if [ -z "$CI" ]; then - echo "Error: This script should only be run in CI environments" - echo "If you need to resolve symlinks locally, run it manually with CI=true" - exit 1 -fi - -echo "Resolving all symlinks in current directory..." - -# Find all symlinks in current directory (including nested ones), excluding gitignored files -git ls-files -z --cached --others --exclude-standard | xargs -0 -I {} sh -c 'test -L "{}" && echo "{}"' | while read -r symlink; do - # Get the target of the symlink - target=$(readlink "$symlink") - - # Check if target is absolute or relative - if [[ "$target" = /* ]]; then - resolved_target="$target" - else - # Resolve relative symlink path - symlink_dir=$(dirname "$symlink") - resolved_target="$symlink_dir/$target" - fi - - echo "Resolving: $symlink -> $resolved_target" - - # Remove the symlink - rm "$symlink" - - # Copy the target to the symlink location - if [ -d "$resolved_target" ]; then - cp -r "$resolved_target" "$symlink" - else - cp "$resolved_target" "$symlink" - fi -done - -echo "All symlinks resolved successfully!" diff --git a/workbench/nitro-v3/workflows/7_full.ts b/workbench/nitro-v3/workflows/7_full.ts new file mode 120000 index 0000000000..660fd8736e --- /dev/null +++ b/workbench/nitro-v3/workflows/7_full.ts @@ -0,0 +1 @@ +../../example/workflows/7_full.ts \ No newline at end of file diff --git a/workbench/nuxt/.gitignore b/workbench/nuxt/.gitignore index 0b1d584c0f..be8f703240 100644 --- a/workbench/nuxt/.gitignore +++ b/workbench/nuxt/.gitignore @@ -5,3 +5,4 @@ manifest.js .output .data .vercel +_workflows.ts diff --git a/workbench/nuxt/_workflows.ts b/workbench/nuxt/_workflows.ts deleted file mode 100644 index 4cc54ee4e0..0000000000 --- a/workbench/nuxt/_workflows.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as demo from './workflows/0_demo.js'; -import * as simple from './workflows/1_simple.js'; -import * as controlFlow from './workflows/2_control_flow.js'; -import * as streams from './workflows/3_streams.js'; -import * as ai from './workflows/4_ai.js'; -import * as hooks from './workflows/5_hooks.js'; -import * as batching from './workflows/6_batching.js'; -import * as duplicate from './workflows/98_duplicate_case.js'; -import * as e2e from './workflows/99_e2e.js'; - -export const allWorkflows = { - 'workflows/0_calc.ts': demo, // 0_demo.ts contains calc function - 'workflows/0_demo.ts': demo, - 'workflows/1_simple.ts': simple, - 'workflows/2_control_flow.ts': controlFlow, - 'workflows/3_streams.ts': streams, - 'workflows/4_ai.ts': ai, - 'workflows/5_hooks.ts': hooks, - 'workflows/6_batching.ts': batching, - 'workflows/98_duplicate_case.ts': duplicate, - 'workflows/99_e2e.ts': e2e, -}; diff --git a/workbench/nuxt/package.json b/workbench/nuxt/package.json index b65b4f3b69..36556b3e07 100644 --- a/workbench/nuxt/package.json +++ b/workbench/nuxt/package.json @@ -5,6 +5,9 @@ "version": "0.0.0", "license": "Apache-2.0", "scripts": { + "generate:workflows": "node ../scripts/generate-workflows-registry.js", + "predev": "pnpm generate:workflows", + "prebuild": "pnpm generate:workflows", "dev": "nuxt dev", "build": "nuxt build", "start": "node .output/server/index.mjs" diff --git a/workbench/nuxt/resolve-symlinks.sh b/workbench/nuxt/resolve-symlinks.sh deleted file mode 100755 index 425bd7296e..0000000000 --- a/workbench/nuxt/resolve-symlinks.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -e - -# Script to recursively resolve all symlinks in the app directory -# This is needed for CI where Next.js dev mode doesn't work well with symlinks - -# Only run in CI -if [ -z "$CI" ]; then - echo "Error: This script should only be run in CI environments" - echo "If you need to resolve symlinks locally, run it manually with CI=true" - exit 1 -fi - -echo "Resolving all symlinks in current directory..." - -# Find all symlinks in current directory (including nested ones), excluding gitignored files -git ls-files -z --cached --others --exclude-standard | xargs -0 -I {} sh -c 'test -L "{}" && echo "{}"' | while read -r symlink; do - # Get the target of the symlink - target=$(readlink "$symlink") - - # Check if target is absolute or relative - if [[ "$target" = /* ]]; then - resolved_target="$target" - else - # Resolve relative symlink path - symlink_dir=$(dirname "$symlink") - resolved_target="$symlink_dir/$target" - fi - - echo "Resolving: $symlink -> $resolved_target" - - # Remove the symlink - rm "$symlink" - - # Copy the target to the symlink location - if [ -d "$resolved_target" ]; then - cp -r "$resolved_target" "$symlink" - else - cp "$resolved_target" "$symlink" - fi -done - -echo "All symlinks resolved successfully!" diff --git a/workbench/nuxt/workflows b/workbench/nuxt/workflows index 24a8054053..876d7a80cb 120000 --- a/workbench/nuxt/workflows +++ b/workbench/nuxt/workflows @@ -1 +1 @@ -../nitro-v2/workflows \ No newline at end of file +../nitro-v3/workflows \ No newline at end of file diff --git a/workbench/scripts/generate-workflows-registry.js b/workbench/scripts/generate-workflows-registry.js new file mode 100644 index 0000000000..23b1dc6c10 --- /dev/null +++ b/workbench/scripts/generate-workflows-registry.js @@ -0,0 +1,110 @@ +#!/usr/bin/env node + +/** + * Auto-generates _workflows.ts registry file for workbenches + * + * Usage: node generate-workflows-registry.js [workflowsDir] [outputPath] + * + * Defaults: + * workflowsDir: ./workflows + * outputPath: ./_workflows.ts + */ + +const fs = require('node:fs'); +const path = require('node:path'); + +// Get arguments or use defaults +const workflowsDir = process.argv[2] || './workflows'; +const outputPath = process.argv[3] || './_workflows.ts'; + +// Calculate relative path from output to workflows directory +const outputDir = path.dirname(outputPath); +const relativeWorkflowsPath = path + .relative(outputDir, workflowsDir) + .replace(/\\/g, '/'); + +// Files to skip +const SKIP_FILES = ['helpers.ts']; +const SKIP_PREFIX = '_'; + +function generateSafeIdentifier(filename) { + // Convert filename to safe JS identifier + // e.g., "1_simple.ts" -> "workflow_1_simple" + return ( + 'workflow_' + filename.replace(/\.ts$/, '').replace(/[^a-zA-Z0-9_]/g, '_') + ); +} + +function generateRegistry() { + // Check if workflows directory exists + if (!fs.existsSync(workflowsDir)) { + console.error(`Error: Workflows directory not found: ${workflowsDir}`); + process.exit(1); + } + + // Read all files from workflows directory + const files = fs + .readdirSync(workflowsDir) + .filter((file) => { + // Only .ts files + if (!file.endsWith('.ts')) return false; + // Skip helpers and files starting with _ + if (SKIP_FILES.includes(file)) return false; + if (file.startsWith(SKIP_PREFIX)) return false; + return true; + }) + .sort(); // Sort for consistent output + + if (files.length === 0) { + console.warn('Warning: No workflow files found to register'); + } + + // Generate imports + const imports = files + .map((file) => { + const identifier = generateSafeIdentifier(file); + // Use relative path from output directory to workflows directory + // Don't add .js extension - let the bundler resolve it + let importPath; + if (relativeWorkflowsPath && relativeWorkflowsPath !== 'workflows') { + importPath = `${relativeWorkflowsPath}/${file.replace(/\.ts$/, '')}`; + } else { + importPath = `./workflows/${file.replace(/\.ts$/, '')}`; + } + return `import * as ${identifier} from '${importPath}';`; + }) + .join('\n'); + + // Generate registry object entries + const registryEntries = files + .map((file) => { + const identifier = generateSafeIdentifier(file); + return ` 'workflows/${file}': ${identifier},`; + }) + .join('\n'); + + // Generate full content + const content = `// Auto-generated by workbench/scripts/generate-workflows-registry.js +// Do not edit this file manually - it will be regenerated on build + +${imports} + +export const allWorkflows = { +${registryEntries} +} as const; +`; + + // Write to output file + fs.writeFileSync(outputPath, content, 'utf-8'); + + console.log(`✓ Generated ${outputPath} with ${files.length} workflow(s)`); + files.forEach((file) => console.log(` - workflows/${file}`)); +} + +// Run the generator +try { + generateRegistry(); +} catch (error) { + console.error('Error generating workflows registry:', error); + process.exit(1); +} diff --git a/workbench/sveltekit/.gitignore b/workbench/sveltekit/.gitignore index 3b462cb0c4..a4b663cf80 100644 --- a/workbench/sveltekit/.gitignore +++ b/workbench/sveltekit/.gitignore @@ -21,3 +21,6 @@ Thumbs.db # Vite vite.config.js.timestamp-* vite.config.ts.timestamp-* + +# Workflow +src/lib/_workflows.ts diff --git a/workbench/sveltekit/package.json b/workbench/sveltekit/package.json index 9a22456a6b..74af9740aa 100644 --- a/workbench/sveltekit/package.json +++ b/workbench/sveltekit/package.json @@ -4,6 +4,9 @@ "version": "0.0.0", "type": "module", "scripts": { + "generate:workflows": "node ../scripts/generate-workflows-registry.js ./src/workflows ./src/lib/_workflows.ts", + "predev": "pnpm generate:workflows", + "prebuild": "pnpm generate:workflows", "dev": "vite dev", "build": "vite build", "start": "vite preview", diff --git a/workbench/sveltekit/resolve-symlinks.sh b/workbench/sveltekit/resolve-symlinks.sh deleted file mode 100755 index 425bd7296e..0000000000 --- a/workbench/sveltekit/resolve-symlinks.sh +++ /dev/null @@ -1,43 +0,0 @@ -#!/bin/bash -set -e - -# Script to recursively resolve all symlinks in the app directory -# This is needed for CI where Next.js dev mode doesn't work well with symlinks - -# Only run in CI -if [ -z "$CI" ]; then - echo "Error: This script should only be run in CI environments" - echo "If you need to resolve symlinks locally, run it manually with CI=true" - exit 1 -fi - -echo "Resolving all symlinks in current directory..." - -# Find all symlinks in current directory (including nested ones), excluding gitignored files -git ls-files -z --cached --others --exclude-standard | xargs -0 -I {} sh -c 'test -L "{}" && echo "{}"' | while read -r symlink; do - # Get the target of the symlink - target=$(readlink "$symlink") - - # Check if target is absolute or relative - if [[ "$target" = /* ]]; then - resolved_target="$target" - else - # Resolve relative symlink path - symlink_dir=$(dirname "$symlink") - resolved_target="$symlink_dir/$target" - fi - - echo "Resolving: $symlink -> $resolved_target" - - # Remove the symlink - rm "$symlink" - - # Copy the target to the symlink location - if [ -d "$resolved_target" ]; then - cp -r "$resolved_target" "$symlink" - else - cp "$resolved_target" "$symlink" - fi -done - -echo "All symlinks resolved successfully!" diff --git a/workbench/sveltekit/src/routes/api/chat/+server.ts b/workbench/sveltekit/src/routes/api/chat/+server.ts index 3e2b41d90a..73efee865d 100644 --- a/workbench/sveltekit/src/routes/api/chat/+server.ts +++ b/workbench/sveltekit/src/routes/api/chat/+server.ts @@ -2,7 +2,7 @@ // TO IMPORT THE WORKFLOWS TO DISCOVER THEM AND WATCH import { json, type RequestHandler } from '@sveltejs/kit'; -import * as workflows from '../../../../workflows/3_streams'; +import * as workflows from '../../../workflows/3_streams'; export const POST: RequestHandler = async ({ request, diff --git a/workbench/sveltekit/src/routes/api/signup/+server.ts b/workbench/sveltekit/src/routes/api/signup/+server.ts index 8e76aaf3a9..c15d48def1 100644 --- a/workbench/sveltekit/src/routes/api/signup/+server.ts +++ b/workbench/sveltekit/src/routes/api/signup/+server.ts @@ -1,6 +1,6 @@ import { json, type RequestHandler } from '@sveltejs/kit'; import { start } from 'workflow/api'; -import { handleUserSignup } from '../../../../workflows/user-signup'; +import { handleUserSignup } from '../../../workflows/user-signup'; export const GET: RequestHandler = async ({ request, diff --git a/workbench/sveltekit/src/routes/api/test-direct-step-call/+server.ts b/workbench/sveltekit/src/routes/api/test-direct-step-call/+server.ts index 1aef582f75..e85d89f00b 100644 --- a/workbench/sveltekit/src/routes/api/test-direct-step-call/+server.ts +++ b/workbench/sveltekit/src/routes/api/test-direct-step-call/+server.ts @@ -2,8 +2,8 @@ // After the SWC compiler changes, step functions in client mode have their directive removed // and keep their original implementation, allowing them to be called as regular async functions -import { json, type RequestHandler } from '@sveltejs/kit'; -import { add } from '../../../../workflows/99_e2e.js'; +import { type RequestHandler } from '@sveltejs/kit'; +import { add } from '../../../workflows/99_e2e'; export const POST: RequestHandler = async ({ request }) => { const body = await request.json(); @@ -15,5 +15,5 @@ export const POST: RequestHandler = async ({ request }) => { const result = await add(x, y); console.log(`add(${x}, ${y}) = ${result}`); - return json({ result }); + return Response.json({ result }); }; diff --git a/workbench/sveltekit/src/routes/api/trigger/+server.ts b/workbench/sveltekit/src/routes/api/trigger/+server.ts index 4b38f05b09..6492f436d2 100644 --- a/workbench/sveltekit/src/routes/api/trigger/+server.ts +++ b/workbench/sveltekit/src/routes/api/trigger/+server.ts @@ -1,47 +1,37 @@ -import { json, type RequestHandler } from '@sveltejs/kit'; +import type { RequestHandler } from '@sveltejs/kit'; import { getRun, start } from 'workflow/api'; -import { hydrateWorkflowArguments } from 'workflow/internal/serialization'; -import * as calcWorkflow from '../../../../workflows/0_calc'; -import * as batchingWorkflow from '../../../../workflows/6_batching'; -import * as duplicateE2e from '../../../../workflows/98_duplicate_case'; -import * as e2eWorkflows from '../../../../workflows/99_e2e'; import { WorkflowRunFailedError, WorkflowRunNotCompletedError, } from 'workflow/internal/errors'; - -const WORKFLOW_MODULES = { - 'workflows/0_calc.ts': calcWorkflow, - 'workflows/6_batching.ts': batchingWorkflow, - 'workflows/98_duplicate_case.ts': duplicateE2e, - 'workflows/99_e2e.ts': e2eWorkflows, -} as const; +import { hydrateWorkflowArguments } from 'workflow/internal/serialization'; +import { allWorkflows } from '$lib/_workflows.js'; export const POST: RequestHandler = async ({ request }) => { const url = new URL(request.url); const workflowFile = url.searchParams.get('workflowFile') || 'workflows/99_e2e.ts'; - const workflowFn = url.searchParams.get('workflowFn') || 'simple'; - - console.log('calling workflow', { workflowFile, workflowFn }); - - const workflows = - WORKFLOW_MODULES[workflowFile as keyof typeof WORKFLOW_MODULES]; + if (!workflowFile) { + return new Response('No workflowFile query parameter provided', { + status: 400, + }); + } + const workflows = allWorkflows[workflowFile as keyof typeof allWorkflows]; if (!workflows) { - return json( - { error: `Workflow file "${workflowFile}" not found` }, - { status: 404 } - ); + return new Response(`Workflow file "${workflowFile}" not found`, { + status: 400, + }); } + const workflowFn = url.searchParams.get('workflowFn') || 'simple'; + if (!workflowFn) { + return new Response('No workflow query parameter provided', { + status: 400, + }); + } const workflow = workflows[workflowFn as keyof typeof workflows]; if (!workflow) { - return json( - { - error: `Workflow "${workflowFn}" not found in "${workflowFile}"`, - }, - { status: 404 } - ); + return new Response(`Workflow "${workflowFn}" not found`, { status: 400 }); } let args: any[] = []; @@ -62,14 +52,12 @@ export const POST: RequestHandler = async ({ request }) => { args = [42]; } } - console.log( - `Starting "${workflowFile}/${workflowFn}" workflow with args: ${args}` - ); + console.log(`Starting "${workflowFn}" workflow with args: ${args}`); try { - const run = await start(workflow as any, args); + const run = await start(workflow as any, args as any); console.log('Run:', run); - return json(run); + return Response.json(run); } catch (err) { console.error(`Failed to start!!`, err); throw err; @@ -117,11 +105,11 @@ export const GET: RequestHandler = async ({ request }) => { 'Content-Type': 'application/octet-stream', }, }) - : json(returnValue); + : Response.json(returnValue); } catch (error) { if (error instanceof Error) { if (WorkflowRunNotCompletedError.is(error)) { - return json( + return Response.json( { ...error, name: error.name, @@ -133,7 +121,7 @@ export const GET: RequestHandler = async ({ request }) => { if (WorkflowRunFailedError.is(error)) { const cause = error.cause; - return json( + return Response.json( { ...error, name: error.name, @@ -153,7 +141,7 @@ export const GET: RequestHandler = async ({ request }) => { 'Unexpected error while getting workflow return value:', error ); - return json( + return Response.json( { error: 'Internal server error', }, diff --git a/workbench/sveltekit/workflows/0_calc.ts b/workbench/sveltekit/src/workflows/0_calc.ts similarity index 100% rename from workbench/sveltekit/workflows/0_calc.ts rename to workbench/sveltekit/src/workflows/0_calc.ts diff --git a/workbench/sveltekit/src/workflows/1_simple.ts b/workbench/sveltekit/src/workflows/1_simple.ts new file mode 120000 index 0000000000..d4ed46b3dc --- /dev/null +++ b/workbench/sveltekit/src/workflows/1_simple.ts @@ -0,0 +1 @@ +../../../example/workflows/1_simple.ts \ No newline at end of file diff --git a/workbench/sveltekit/src/workflows/3_streams.ts b/workbench/sveltekit/src/workflows/3_streams.ts new file mode 120000 index 0000000000..d5796fa17a --- /dev/null +++ b/workbench/sveltekit/src/workflows/3_streams.ts @@ -0,0 +1 @@ +../../../example/workflows/3_streams.ts \ No newline at end of file diff --git a/workbench/sveltekit/src/workflows/6_batching.ts b/workbench/sveltekit/src/workflows/6_batching.ts new file mode 120000 index 0000000000..fa158187df --- /dev/null +++ b/workbench/sveltekit/src/workflows/6_batching.ts @@ -0,0 +1 @@ +../../../example/workflows/6_batching.ts \ No newline at end of file diff --git a/workbench/sveltekit/src/workflows/7_full.ts b/workbench/sveltekit/src/workflows/7_full.ts new file mode 120000 index 0000000000..953dd0944e --- /dev/null +++ b/workbench/sveltekit/src/workflows/7_full.ts @@ -0,0 +1 @@ +../../../example/workflows/7_full.ts \ No newline at end of file diff --git a/workbench/sveltekit/src/workflows/98_duplicate_case.ts b/workbench/sveltekit/src/workflows/98_duplicate_case.ts new file mode 120000 index 0000000000..9fd0dfdf3b --- /dev/null +++ b/workbench/sveltekit/src/workflows/98_duplicate_case.ts @@ -0,0 +1 @@ +../../../example/workflows/98_duplicate_case.ts \ No newline at end of file diff --git a/workbench/sveltekit/src/workflows/99_e2e.ts b/workbench/sveltekit/src/workflows/99_e2e.ts new file mode 120000 index 0000000000..7e16475de2 --- /dev/null +++ b/workbench/sveltekit/src/workflows/99_e2e.ts @@ -0,0 +1 @@ +../../../example/workflows/99_e2e.ts \ No newline at end of file diff --git a/workbench/sveltekit/src/workflows/helpers.ts b/workbench/sveltekit/src/workflows/helpers.ts new file mode 120000 index 0000000000..d155ce1c45 --- /dev/null +++ b/workbench/sveltekit/src/workflows/helpers.ts @@ -0,0 +1 @@ +../../../example/workflows/helpers.ts \ No newline at end of file diff --git a/workbench/sveltekit/src/workflows/user-signup.ts b/workbench/sveltekit/src/workflows/user-signup.ts new file mode 100644 index 0000000000..173c7196e0 --- /dev/null +++ b/workbench/sveltekit/src/workflows/user-signup.ts @@ -0,0 +1,43 @@ +import { createWebhook, sleep } from 'workflow'; + +export async function handleUserSignup(email: string) { + 'use workflow'; + + const user = await createUser(email); + await sendWelcomeEmail(user); + + await sleep('5s'); + + const webhook = createWebhook(); + await sendOnboardingEmail(user, webhook.url); + + await webhook; + console.log('Webhook Resolved'); + + return { userId: user.id, status: 'onboarded' }; +} + +async function createUser(email: string) { + 'use step'; + + console.log(`Creating a new user with email: ${email}`); + + return { id: crypto.randomUUID(), email }; +} + +async function sendWelcomeEmail(user: { id: string; email: string }) { + 'use step'; + + console.log(`Sending welcome email to user: ${user.id}`); +} + +async function sendOnboardingEmail( + user: { id: string; email: string }, + callback: string +) { + 'use step'; + + console.log(`Sending onboarding email to user: ${user.id}`); + + console.log(`Click this link to resolve the webhook: ${callback}`); +} diff --git a/workbench/vite/.gitignore b/workbench/vite/.gitignore index 178daca81d..c80a833d3b 100644 --- a/workbench/vite/.gitignore +++ b/workbench/vite/.gitignore @@ -4,3 +4,4 @@ manifest.js .output .data .vercel +_workflows.ts diff --git a/workbench/vite/_workflows.ts b/workbench/vite/_workflows.ts deleted file mode 120000 index 217286881e..0000000000 --- a/workbench/vite/_workflows.ts +++ /dev/null @@ -1 +0,0 @@ -../nitro-v3/_workflows.ts \ No newline at end of file diff --git a/workbench/vite/package.json b/workbench/vite/package.json index e7828db22f..5543f36910 100644 --- a/workbench/vite/package.json +++ b/workbench/vite/package.json @@ -5,8 +5,12 @@ "version": "0.0.0", "license": "Apache-2.0", "scripts": { + "generate:workflows": "node ../scripts/generate-workflows-registry.js", + "predev": "pnpm generate:workflows", + "prebuild": "pnpm generate:workflows", "dev": "vite dev", - "build": "vite build" + "build": "vite build", + "start": "node .output/server/index.mjs" }, "devDependencies": { "ai": "catalog:", diff --git a/workbench/vite/routes b/workbench/vite/routes deleted file mode 120000 index f2c088d596..0000000000 --- a/workbench/vite/routes +++ /dev/null @@ -1 +0,0 @@ -../nitro-v3/routes \ No newline at end of file diff --git a/workbench/vite/routes/api/chat.post.ts b/workbench/vite/routes/api/chat.post.ts new file mode 100644 index 0000000000..c534d8d4b3 --- /dev/null +++ b/workbench/vite/routes/api/chat.post.ts @@ -0,0 +1,9 @@ +// THIS FILE IS JUST FOR TESTING HMR AS AN ENTRY NEEDS +// TO IMPORT THE WORKFLOWS TO DISCOVER THEM AND WATCH + +import * as workflows from '../../workflows/3_streams.js'; + +export default async ({ req }: { req: Request }) => { + console.log(workflows); + return Response.json('hello world'); +}; diff --git a/workbench/vite/routes/api/hook.post.ts b/workbench/vite/routes/api/hook.post.ts new file mode 100644 index 0000000000..ecbdc636cd --- /dev/null +++ b/workbench/vite/routes/api/hook.post.ts @@ -0,0 +1,25 @@ +import { getHookByToken, resumeHook } from 'workflow/api'; + +export default async ({ req }: { req: Request }) => { + const { token, data } = await req.json(); + + let hook: Awaited>; + try { + hook = await getHookByToken(token); + console.log('hook', hook); + } catch (error) { + console.log('error during getHookByToken', error); + // TODO: `WorkflowAPIError` is not exported, so for now + // 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, { + ...data, + // @ts-expect-error metadata is not typed + customData: hook.metadata?.customData, + }); + + return Response.json(hook); +}; diff --git a/workbench/vite/routes/api/test-direct-step-call.post.ts b/workbench/vite/routes/api/test-direct-step-call.post.ts new file mode 100644 index 0000000000..543f8201da --- /dev/null +++ b/workbench/vite/routes/api/test-direct-step-call.post.ts @@ -0,0 +1,18 @@ +// This route tests calling step functions directly outside of any workflow context +// After the SWC compiler changes, step functions in client mode have their directive removed +// and keep their original implementation, allowing them to be called as regular async functions + +import { add } from '../../workflows/99_e2e'; + +export default async ({ req }: { req: Request }) => { + const body = await req.json(); + const { x, y } = body; + + console.log(`Calling step function directly with x=${x}, y=${y}`); + + // Call step function directly as a regular async function (no workflow context) + const result = await add(x, y); + console.log(`add(${x}, ${y}) = ${result}`); + + return Response.json({ result }); +}; diff --git a/workbench/vite/routes/api/trigger.get.ts b/workbench/vite/routes/api/trigger.get.ts new file mode 100644 index 0000000000..a7ef468e6e --- /dev/null +++ b/workbench/vite/routes/api/trigger.get.ts @@ -0,0 +1,90 @@ +import { getRun } from 'workflow/api'; +import { + WorkflowRunFailedError, + WorkflowRunNotCompletedError, +} from 'workflow/internal/errors'; + +export default async ({ url }: { req: Request; url: URL }) => { + const runId = url.searchParams.get('runId'); + if (!runId) { + return new Response('No runId provided', { status: 400 }); + } + + const outputStreamParam = url.searchParams.get('output-stream'); + if (outputStreamParam) { + const namespace = outputStreamParam === '1' ? undefined : outputStreamParam; + const run = getRun(runId); + const stream = run.getReadable({ + namespace, + }); + // Add JSON framing to the stream, wrapping binary data in base64 + const streamWithFraming = new TransformStream({ + transform(chunk, controller) { + const data = + chunk instanceof Uint8Array + ? { data: Buffer.from(chunk).toString('base64') } + : chunk; + controller.enqueue(`${JSON.stringify(data)}\n`); + }, + }); + return new Response(stream.pipeThrough(streamWithFraming), { + headers: { + 'Content-Type': 'application/octet-stream', + }, + }); + } + + try { + const run = getRun(runId); + const returnValue = await run.returnValue; + console.log('Return value:', returnValue); + return returnValue instanceof ReadableStream + ? new Response(returnValue, { + headers: { + 'Content-Type': 'application/octet-stream', + }, + }) + : Response.json(returnValue); + } catch (error) { + if (error instanceof Error) { + if (WorkflowRunNotCompletedError.is(error)) { + return Response.json( + { + ...error, + name: error.name, + message: error.message, + }, + { status: 202 } + ); + } + + if (WorkflowRunFailedError.is(error)) { + const cause = error.cause; + return Response.json( + { + ...error, + name: error.name, + message: error.message, + cause: { + message: cause.message, + stack: cause.stack, + code: cause.code, + }, + }, + { status: 400 } + ); + } + } + + console.error( + 'Unexpected error while getting workflow return value:', + error + ); + return Response.json( + { + error: 'Internal server error', + }, + { status: 500 } + ); + } +}; diff --git a/workbench/vite/routes/api/trigger.post.ts b/workbench/vite/routes/api/trigger.post.ts new file mode 100644 index 0000000000..2cf0025657 --- /dev/null +++ b/workbench/vite/routes/api/trigger.post.ts @@ -0,0 +1,59 @@ +import { start } from 'workflow/api'; +import { hydrateWorkflowArguments } from 'workflow/internal/serialization'; +import { allWorkflows } from '../../_workflows.js'; + +export default async ({ req, url }: { req: Request; url: URL }) => { + const workflowFile = + url.searchParams.get('workflowFile') || 'workflows/99_e2e.ts'; + if (!workflowFile) { + return new Response('No workflowFile query parameter provided', { + status: 400, + }); + } + const workflows = allWorkflows[workflowFile as keyof typeof allWorkflows]; + if (!workflows) { + return new Response(`Workflow file "${workflowFile}" not found`, { + status: 400, + }); + } + + const workflowFn = url.searchParams.get('workflowFn') || 'simple'; + if (!workflowFn) { + return new Response('No workflow query parameter provided', { + status: 400, + }); + } + const workflow = workflows[workflowFn as keyof typeof workflows]; + if (!workflow) { + return new Response(`Workflow "${workflowFn}" not found`, { status: 400 }); + } + + let args: any[] = []; + + // Args from query string + const argsParam = url.searchParams.get('args'); + if (argsParam) { + args = argsParam.split(',').map((arg) => { + const num = parseFloat(arg); + return Number.isNaN(num) ? arg.trim() : num; + }); + } else { + // Args from body + const body = await req.text(); + if (body) { + args = hydrateWorkflowArguments(JSON.parse(body), globalThis); + } else { + args = [42]; + } + } + console.log(`Starting "${workflowFn}" workflow with args: ${args}`); + + try { + const run = await start(workflow as any, args as any); + console.log('Run:', run); + return Response.json(run); + } catch (err) { + console.error(`Failed to start!!`, err); + throw err; + } +}; diff --git a/workbench/vite/vite.config.ts b/workbench/vite/vite.config.ts index a8b609d6c1..78aa314378 100644 --- a/workbench/vite/vite.config.ts +++ b/workbench/vite/vite.config.ts @@ -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({