From 07d286ba51f033eba1f0bab2004c293ed65458db Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Mon, 3 Nov 2025 22:15:02 -0800 Subject: [PATCH 01/16] add hmr and dev tests for nitro and sveltekit --- .github/workflows/tests.yml | 4 +- packages/core/e2e/dev-test-factory.ts | 136 +++++++++++++++++ packages/core/e2e/next-dev.test.ts | 138 +----------------- packages/core/e2e/nitro-dev.test.ts | 8 + packages/core/e2e/sveltekit-dev.test.ts | 8 + packages/nitro/src/index.ts | 10 +- workbench/example/workflows/99_e2e.ts | 10 ++ workbench/nitro-v3/routes/api/chat.post.ts | 9 ++ .../sveltekit/src/routes/api/chat/+server.ts | 14 ++ workbench/sveltekit/workflows/streams.ts | 1 + 10 files changed, 203 insertions(+), 135 deletions(-) create mode 100644 packages/core/e2e/dev-test-factory.ts create mode 100644 packages/core/e2e/nitro-dev.test.ts create mode 100644 packages/core/e2e/sveltekit-dev.test.ts create mode 100644 workbench/nitro-v3/routes/api/chat.post.ts create mode 100644 workbench/sveltekit/src/routes/api/chat/+server.ts create mode 120000 workbench/sveltekit/workflows/streams.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 2d9a62098..18dd7dbe6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -191,7 +191,7 @@ jobs: - name: Run E2E Tests (Nitro) if: matrix.app.name == 'nitro' - run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/e2e.test.ts + run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/nitro-dev.test.ts && pnpm run test:e2e env: APP_NAME: ${{ matrix.app.name }} DEPLOYMENT_URL: "http://localhost:3000" @@ -203,7 +203,7 @@ jobs: DEPLOYMENT_URL: "http://localhost:3000" - name: Run E2E Tests (SvelteKit) if: matrix.app.name == 'sveltekit' - run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/e2e.test.ts + run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/sveltekit-dev.test.ts && pnpm run test:e2e env: APP_NAME: ${{ matrix.app.name }} DEPLOYMENT_URL: "http://localhost:3000" diff --git a/packages/core/e2e/dev-test-factory.ts b/packages/core/e2e/dev-test-factory.ts new file mode 100644 index 000000000..622efd444 --- /dev/null +++ b/packages/core/e2e/dev-test-factory.ts @@ -0,0 +1,136 @@ +import fs from 'fs/promises'; +import path from 'path'; +import { afterEach, describe, expect, test } from 'vitest'; +import { getWorkbenchAppPath } from './utils'; + +export interface DevTestConfig { + generatedStepPath: string; + generatedWorkflowPath: string; + apiFilePath: string; + apiFileImportPath: string; +} + +export function createDevTests(config: DevTestConfig) { + describe('dev e2e', () => { + const appPath = getWorkbenchAppPath(); + const generatedStep = path.join(appPath, config.generatedStepPath); + const generatedWorkflow = path.join(appPath, config.generatedWorkflowPath); + + const restoreFiles: Array<{ path: string; content: string }> = []; + + afterEach(async () => { + await Promise.all( + restoreFiles.map(async (item) => { + if (item.content === '') { + await fs.unlink(item.path); + } else { + await fs.writeFile(item.path, item.content); + } + }) + ); + restoreFiles.length = 0; + }); + + test('should rebuild on workflow change', { timeout: 10_000 }, async () => { + const workflowFile = path.join(appPath, 'workflows', '99_e2e.ts'); + + const content = await fs.readFile(workflowFile, 'utf8'); + + await fs.writeFile( + workflowFile, + ` + ${content} + + export async function myNewWorkflow() { + 'use workflow' + return 'hello world' + } + ` + ); + restoreFiles.push({ path: workflowFile, content }); + + while (true) { + try { + const workflowContent = await fs.readFile(generatedWorkflow, 'utf8'); + expect(workflowContent).toContain('myNewWorkflow'); + break; + } catch (_) { + await new Promise((res) => setTimeout(res, 1_000)); + } + } + }); + + test('should rebuild on step change', { timeout: 10_000 }, async () => { + const stepFile = path.join(appPath, 'workflows', '99_e2e.ts'); + + const content = await fs.readFile(stepFile, 'utf8'); + + await fs.writeFile( + stepFile, + ` + ${content} + + export async function myNewStep() { + 'use step' + return 'hello world' + } + ` + ); + restoreFiles.push({ path: stepFile, content }); + + while (true) { + try { + const workflowContent = await fs.readFile(generatedStep, 'utf8'); + expect(workflowContent).toContain('myNewStep'); + break; + } catch (_) { + await new Promise((res) => setTimeout(res, 1_000)); + } + } + }); + + test( + 'should rebuild on adding workflow file', + { timeout: 10_000 }, + async () => { + const workflowFile = path.join(appPath, 'workflows', 'new-workflow.ts'); + + await fs.writeFile( + workflowFile, + ` + export async function newWorkflowFile() { + 'use workflow' + return 'hello world' + } + ` + ); + restoreFiles.push({ path: workflowFile, content: '' }); + const apiFile = path.join(appPath, config.apiFilePath); + + const apiFileContent = await fs.readFile(apiFile, 'utf8'); + restoreFiles.push({ path: apiFile, content: apiFileContent }); + + await fs.writeFile( + apiFile, + ` + import '${config.apiFileImportPath}/workflows/new-workflow'; + ${apiFileContent} + ` + ); + + while (true) { + try { + const workflowContent = await fs.readFile( + generatedWorkflow, + 'utf8' + ); + expect(workflowContent).toContain('newWorkflowFile'); + break; + } catch (_) { + await new Promise((res) => setTimeout(res, 1_000)); + } + } + } + ); + }); +} diff --git a/packages/core/e2e/next-dev.test.ts b/packages/core/e2e/next-dev.test.ts index 0c2d190f5..9722ccd3f 100644 --- a/packages/core/e2e/next-dev.test.ts +++ b/packages/core/e2e/next-dev.test.ts @@ -1,134 +1,8 @@ -import fs from 'fs/promises'; -import path from 'path'; -import { afterEach, describe, expect, test } from 'vitest'; -import { getWorkbenchAppPath } from './utils'; +import { createDevTests } from './dev-test-factory'; -describe('dev e2e', () => { - const appPath = getWorkbenchAppPath(); - const generatedStep = path.join( - appPath, - 'app', - '.well-known/workflow/v1/step', - 'route.js' - ); - const generatedWorkflow = path.join( - appPath, - 'app', - '.well-known/workflow/v1/flow', - 'route.js' - ); - - const restoreFiles: Array<{ path: string; content: string }> = []; - - afterEach(async () => { - await Promise.all( - restoreFiles.map(async (item) => { - if (item.content === '') { - await fs.unlink(item.path); - } else { - await fs.writeFile(item.path, item.content); - } - }) - ); - restoreFiles.length = 0; - }); - - test('should rebuild on workflow change', { timeout: 10_000 }, async () => { - const workflowFile = path.join(appPath, 'workflows', 'streams.ts'); - - const content = await fs.readFile(workflowFile, 'utf8'); - - await fs.writeFile( - workflowFile, - ` - ${content} - - export async function myNewWorkflow() { - 'use workflow' - return 'hello world' - } - ` - ); - restoreFiles.push({ path: workflowFile, content }); - - while (true) { - try { - const workflowContent = await fs.readFile(generatedWorkflow, 'utf8'); - expect(workflowContent).toContain('myNewWorkflow'); - break; - } catch (_) { - await new Promise((res) => setTimeout(res, 1_000)); - } - } - }); - - test('should rebuild on step change', { timeout: 10_000 }, async () => { - const stepFile = path.join(appPath, 'workflows', 'streams.ts'); - - const content = await fs.readFile(stepFile, 'utf8'); - - await fs.writeFile( - stepFile, - ` - ${content} - - export async function myNewStep() { - 'use step' - return 'hello world' - } - ` - ); - restoreFiles.push({ path: stepFile, content }); - - while (true) { - try { - const workflowContent = await fs.readFile(generatedStep, 'utf8'); - expect(workflowContent).toContain('myNewStep'); - break; - } catch (_) { - await new Promise((res) => setTimeout(res, 1_000)); - } - } - }); - - test( - 'should rebuild on adding workflow file', - { timeout: 10_000 }, - async () => { - const workflowFile = path.join(appPath, 'workflows', 'new-workflow.ts'); - - await fs.writeFile( - workflowFile, - ` - export async function newWorkflowFile() { - 'use workflow' - return 'hello world' - } - ` - ); - restoreFiles.push({ path: workflowFile, content: '' }); - const chatApiFile = path.join(appPath, 'app', 'api', 'chat', 'route.ts'); - - const chatApiFileContent = await fs.readFile(chatApiFile, 'utf8'); - restoreFiles.push({ path: chatApiFile, content: chatApiFileContent }); - - await fs.writeFile( - chatApiFile, - ` - import '../../../workflows/new-workflow'; - ${chatApiFileContent} - ` - ); - - while (true) { - try { - const workflowContent = await fs.readFile(generatedWorkflow, 'utf8'); - expect(workflowContent).toContain('newWorkflowFile'); - break; - } catch (_) { - await new Promise((res) => setTimeout(res, 1_000)); - } - } - } - ); +createDevTests({ + generatedStepPath: 'app/.well-known/workflow/v1/step/route.js', + generatedWorkflowPath: 'app/.well-known/workflow/v1/flow/route.js', + apiFilePath: 'app/api/chat/route.ts', + apiFileImportPath: '../../../', }); diff --git a/packages/core/e2e/nitro-dev.test.ts b/packages/core/e2e/nitro-dev.test.ts new file mode 100644 index 000000000..49c208e73 --- /dev/null +++ b/packages/core/e2e/nitro-dev.test.ts @@ -0,0 +1,8 @@ +import { createDevTests } from './dev-test-factory'; + +createDevTests({ + generatedStepPath: '.nitro/workflow/steps.mjs', + generatedWorkflowPath: '.nitro/workflow/workflows.mjs', + apiFilePath: 'routes/api/chat.post.ts', + apiFileImportPath: '../../', +}); diff --git a/packages/core/e2e/sveltekit-dev.test.ts b/packages/core/e2e/sveltekit-dev.test.ts new file mode 100644 index 000000000..17a1bd972 --- /dev/null +++ b/packages/core/e2e/sveltekit-dev.test.ts @@ -0,0 +1,8 @@ +import { createDevTests } from './dev-test-factory'; + +createDevTests({ + generatedStepPath: 'src/routes/.well-known/workflow/v1/step/+server.js', + generatedWorkflowPath: 'src/routes/.well-known/workflow/v1/flow/+server.js', + apiFilePath: 'src/routes/api/chat/+server.ts', + apiFileImportPath: '../../../../', +}); diff --git a/packages/nitro/src/index.ts b/packages/nitro/src/index.ts index 43535543c..acb33b9a3 100644 --- a/packages/nitro/src/index.ts +++ b/packages/nitro/src/index.ts @@ -31,10 +31,18 @@ export default { // Generate local bundles for dev and local prod if (!isVercelDeploy) { + const builder = new LocalBuilder(nitro); nitro.hooks.hook('build:before', async () => { - await new LocalBuilder(nitro).build(); + await builder.build(); }); + // Allows for HMR + if (nitro.options.dev) { + nitro.hooks.hook('dev:reload', async () => { + await builder.build(); + }); + } + addVirtualHandler( nitro, '/.well-known/workflow/v1/webhook/:token', diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index b6dd9cd2f..462135c00 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -435,3 +435,13 @@ export async function retryableAndFatalErrorWorkflow() { return { retryableResult, gotFatalError }; } + +export async function myNewWorkflow() { + 'use workflow'; + return 'hello world'; +} + +export async function myNewStep() { + 'use step'; + return 'hello world'; +} diff --git a/workbench/nitro-v3/routes/api/chat.post.ts b/workbench/nitro-v3/routes/api/chat.post.ts new file mode 100644 index 000000000..c534d8d4b --- /dev/null +++ b/workbench/nitro-v3/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/sveltekit/src/routes/api/chat/+server.ts b/workbench/sveltekit/src/routes/api/chat/+server.ts new file mode 100644 index 000000000..99ba5524b --- /dev/null +++ b/workbench/sveltekit/src/routes/api/chat/+server.ts @@ -0,0 +1,14 @@ +// THIS FILE IS JUST FOR TESTING HMR AS AN ENTRY NEEDS +// TO IMPORT THE WORKFLOWS TO DISCOVER THEM AND WATCH + +import { json, type RequestHandler } from '@sveltejs/kit'; +import * as workflows from '../../../../workflows/streams'; + +export const POST: RequestHandler = async ({ + request, +}: { + request: Request; +}) => { + console.log(workflows); + return json('hello world'); +}; diff --git a/workbench/sveltekit/workflows/streams.ts b/workbench/sveltekit/workflows/streams.ts new file mode 120000 index 000000000..b4f749998 --- /dev/null +++ b/workbench/sveltekit/workflows/streams.ts @@ -0,0 +1 @@ +../../nextjs-turbopack/workflows/streams.ts \ No newline at end of file From 1fe802ccbb3d6c24660e85400c37c8bea862e632 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Mon, 3 Nov 2025 22:21:30 -0800 Subject: [PATCH 02/16] changeset --- .changeset/tiny-coins-sip.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/tiny-coins-sip.md diff --git a/.changeset/tiny-coins-sip.md b/.changeset/tiny-coins-sip.md new file mode 100644 index 000000000..722531070 --- /dev/null +++ b/.changeset/tiny-coins-sip.md @@ -0,0 +1,5 @@ +--- +"@workflow/nitro": patch +--- + +Add HMR to nitro integration From d146424d6f232b50d152c39217901658eaf29f86 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Mon, 3 Nov 2025 22:23:09 -0800 Subject: [PATCH 03/16] revert: e2e testing code --- workbench/example/workflows/99_e2e.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/workbench/example/workflows/99_e2e.ts b/workbench/example/workflows/99_e2e.ts index 462135c00..b6dd9cd2f 100644 --- a/workbench/example/workflows/99_e2e.ts +++ b/workbench/example/workflows/99_e2e.ts @@ -435,13 +435,3 @@ export async function retryableAndFatalErrorWorkflow() { return { retryableResult, gotFatalError }; } - -export async function myNewWorkflow() { - 'use workflow'; - return 'hello world'; -} - -export async function myNewStep() { - 'use step'; - return 'hello world'; -} From 142144c198c12a3c1924708dbb8a5ac93397974d Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Mon, 3 Nov 2025 23:26:14 -0800 Subject: [PATCH 04/16] add streams.ts to workbench apps --- workbench/nextjs-turbopack/workflows/streams.ts | 10 ++++++++++ workbench/nitro-v3/package.json | 3 +++ workbench/nitro-v3/routes/api/chat.post.ts | 2 +- workbench/nitro-v3/workflows/streams.ts | 1 + 4 files changed, 15 insertions(+), 1 deletion(-) create mode 120000 workbench/nitro-v3/workflows/streams.ts diff --git a/workbench/nextjs-turbopack/workflows/streams.ts b/workbench/nextjs-turbopack/workflows/streams.ts index 15c1123a4..236f0c232 100644 --- a/workbench/nextjs-turbopack/workflows/streams.ts +++ b/workbench/nextjs-turbopack/workflows/streams.ts @@ -38,3 +38,13 @@ export async function write( writer.close(); } } + +export async function myNewStep() { + 'use step'; + return 'hello world'; +} + +export async function myNewWorkflow() { + 'use workflow'; + return 'hello world'; +} diff --git a/workbench/nitro-v3/package.json b/workbench/nitro-v3/package.json index f6b1e6365..bf388b6df 100644 --- a/workbench/nitro-v3/package.json +++ b/workbench/nitro-v3/package.json @@ -16,5 +16,8 @@ "openai": "^6.1.0", "workflow": "workspace:*", "zod": "catalog:" + }, + "dependencies": { + "@node-rs/xxhash": "1.7.6" } } diff --git a/workbench/nitro-v3/routes/api/chat.post.ts b/workbench/nitro-v3/routes/api/chat.post.ts index c534d8d4b..f1c1bec09 100644 --- a/workbench/nitro-v3/routes/api/chat.post.ts +++ b/workbench/nitro-v3/routes/api/chat.post.ts @@ -1,7 +1,7 @@ // 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'; +import * as workflows from '../../workflows/streams.js'; export default async ({ req }: { req: Request }) => { console.log(workflows); diff --git a/workbench/nitro-v3/workflows/streams.ts b/workbench/nitro-v3/workflows/streams.ts new file mode 120000 index 000000000..b4f749998 --- /dev/null +++ b/workbench/nitro-v3/workflows/streams.ts @@ -0,0 +1 @@ +../../nextjs-turbopack/workflows/streams.ts \ No newline at end of file From e7ae011f6bf019008794345e2eb6dd905da53342 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Mon, 3 Nov 2025 23:27:39 -0800 Subject: [PATCH 05/16] fix: test confnigs --- packages/core/e2e/dev-test-factory.ts | 51 +++++++++++-------------- packages/core/e2e/next-dev.test.ts | 2 +- packages/core/e2e/nitro-dev.test.ts | 2 +- packages/core/e2e/sveltekit-dev.test.ts | 2 +- pnpm-lock.yaml | 4 ++ 5 files changed, 30 insertions(+), 31 deletions(-) diff --git a/packages/core/e2e/dev-test-factory.ts b/packages/core/e2e/dev-test-factory.ts index 622efd444..d660c899d 100644 --- a/packages/core/e2e/dev-test-factory.ts +++ b/packages/core/e2e/dev-test-factory.ts @@ -32,20 +32,19 @@ export function createDevTests(config: DevTestConfig) { }); test('should rebuild on workflow change', { timeout: 10_000 }, async () => { - const workflowFile = path.join(appPath, 'workflows', '99_e2e.ts'); + const workflowFile = path.join(appPath, 'workflows', 'streams.ts'); const content = await fs.readFile(workflowFile, 'utf8'); await fs.writeFile( workflowFile, - ` - ${content} - - export async function myNewWorkflow() { - 'use workflow' - return 'hello world' - } - ` + `${content} + +export async function myNewWorkflow() { + 'use workflow' + return 'hello world' +} +` ); restoreFiles.push({ path: workflowFile, content }); @@ -61,20 +60,19 @@ export function createDevTests(config: DevTestConfig) { }); test('should rebuild on step change', { timeout: 10_000 }, async () => { - const stepFile = path.join(appPath, 'workflows', '99_e2e.ts'); + const stepFile = path.join(appPath, 'workflows', 'streams.ts'); const content = await fs.readFile(stepFile, 'utf8'); await fs.writeFile( stepFile, - ` - ${content} - - export async function myNewStep() { - 'use step' - return 'hello world' - } - ` + `${content} + +export async function myNewStep() { + 'use step' + return 'hello world' +} +` ); restoreFiles.push({ path: stepFile, content }); @@ -97,12 +95,11 @@ export function createDevTests(config: DevTestConfig) { await fs.writeFile( workflowFile, - ` - export async function newWorkflowFile() { - 'use workflow' - return 'hello world' - } - ` + `export async function newWorkflowFile() { + 'use workflow' + return 'hello world' +} +` ); restoreFiles.push({ path: workflowFile, content: '' }); const apiFile = path.join(appPath, config.apiFilePath); @@ -112,10 +109,8 @@ export function createDevTests(config: DevTestConfig) { await fs.writeFile( apiFile, - ` - import '${config.apiFileImportPath}/workflows/new-workflow'; - ${apiFileContent} - ` + `import '${config.apiFileImportPath}/workflows/new-workflow'; +${apiFileContent}` ); while (true) { diff --git a/packages/core/e2e/next-dev.test.ts b/packages/core/e2e/next-dev.test.ts index 9722ccd3f..77a85022c 100644 --- a/packages/core/e2e/next-dev.test.ts +++ b/packages/core/e2e/next-dev.test.ts @@ -4,5 +4,5 @@ createDevTests({ generatedStepPath: 'app/.well-known/workflow/v1/step/route.js', generatedWorkflowPath: 'app/.well-known/workflow/v1/flow/route.js', apiFilePath: 'app/api/chat/route.ts', - apiFileImportPath: '../../../', + apiFileImportPath: '../../..', }); diff --git a/packages/core/e2e/nitro-dev.test.ts b/packages/core/e2e/nitro-dev.test.ts index 49c208e73..09141e5a7 100644 --- a/packages/core/e2e/nitro-dev.test.ts +++ b/packages/core/e2e/nitro-dev.test.ts @@ -4,5 +4,5 @@ createDevTests({ generatedStepPath: '.nitro/workflow/steps.mjs', generatedWorkflowPath: '.nitro/workflow/workflows.mjs', apiFilePath: 'routes/api/chat.post.ts', - apiFileImportPath: '../../', + apiFileImportPath: '../..', }); diff --git a/packages/core/e2e/sveltekit-dev.test.ts b/packages/core/e2e/sveltekit-dev.test.ts index 17a1bd972..e005bcd69 100644 --- a/packages/core/e2e/sveltekit-dev.test.ts +++ b/packages/core/e2e/sveltekit-dev.test.ts @@ -4,5 +4,5 @@ createDevTests({ generatedStepPath: 'src/routes/.well-known/workflow/v1/step/+server.js', generatedWorkflowPath: 'src/routes/.well-known/workflow/v1/flow/+server.js', apiFilePath: 'src/routes/api/chat/+server.ts', - apiFileImportPath: '../../../../', + apiFileImportPath: '../../../..', }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01614c57d..9d7eaf3f5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1247,6 +1247,10 @@ importers: version: 4.1.11 workbench/nitro-v3: + dependencies: + '@node-rs/xxhash': + specifier: 1.7.6 + version: 1.7.6 devDependencies: ai: specifier: 'catalog:' From b692b9c259b03476c8355b4c9bebb8b9340bbf61 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Mon, 3 Nov 2025 23:28:03 -0800 Subject: [PATCH 06/16] fix: hmr failing on new files for sveltekit plugin --- packages/sveltekit/src/plugin.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/packages/sveltekit/src/plugin.ts b/packages/sveltekit/src/plugin.ts index f4c529af1..308d75d37 100644 --- a/packages/sveltekit/src/plugin.ts +++ b/packages/sveltekit/src/plugin.ts @@ -1,7 +1,7 @@ import { relative } from 'node:path'; import { transform } from '@swc/core'; import { resolveModulePath } from 'exsolve'; -import type { HmrContext, Plugin } from 'vite'; +import type { HotUpdateOptions, Plugin } from 'vite'; import { SvelteKitBuilder } from './builder.js'; export function workflowPlugin(): Plugin { @@ -92,13 +92,9 @@ export function workflowPlugin(): Plugin { builder = new SvelteKitBuilder(); }, - async buildStart() { - await builder.build(); - }, - // TODO: Move this to @workflow/vite or something since this is vite specific - async handleHotUpdate(ctx: HmrContext) { - const { file, server, read } = ctx; + 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)$/; From 8ce6a67fb265f6e15dbcdd19d8df5387b2a2a2cb Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Mon, 3 Nov 2025 23:31:02 -0800 Subject: [PATCH 07/16] lockfile --- pnpm-lock.yaml | 8 ++++++++ workbench/vite/package.json | 3 +++ 2 files changed, 11 insertions(+) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d7eaf3f5..7b8674456 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1199,6 +1199,10 @@ importers: version: 5.9.3 workbench/nitro: + dependencies: + '@node-rs/xxhash': + specifier: 1.7.6 + version: 1.7.6 devDependencies: ai: specifier: 'catalog:' @@ -1366,6 +1370,10 @@ importers: version: 1.0.0(vite@7.1.12(@types/node@24.6.2)(jiti@2.6.1)(lightningcss@1.30.1)(terser@5.44.0)(tsx@4.20.6)(yaml@2.8.0)) workbench/vite: + dependencies: + '@node-rs/xxhash': + specifier: 1.7.6 + version: 1.7.6 devDependencies: ai: specifier: 'catalog:' diff --git a/workbench/vite/package.json b/workbench/vite/package.json index d4669d022..e7828db22 100644 --- a/workbench/vite/package.json +++ b/workbench/vite/package.json @@ -16,5 +16,8 @@ "vite": "^7.1.12", "workflow": "workspace:*", "zod": "catalog:" + }, + "dependencies": { + "@node-rs/xxhash": "1.7.6" } } From 65d2c758a1d0304e3a86abda18135609e758f93c Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Mon, 3 Nov 2025 23:52:58 -0800 Subject: [PATCH 08/16] switch testing to use 3_stream.ts --- packages/core/e2e/dev-test-factory.ts | 10 ++-- .../nextjs-turbopack/app/api/chat/route.ts | 2 +- .../nextjs-turbopack/workflows/3_streams.ts | 1 + .../nextjs-turbopack/workflows/streams.ts | 50 ------------------- workbench/nitro-v3/routes/api/chat.post.ts | 2 +- workbench/nitro-v3/workflows/streams.ts | 1 - workbench/sveltekit/workflows/3_streams.ts | 1 + workbench/sveltekit/workflows/streams.ts | 1 - 8 files changed, 11 insertions(+), 57 deletions(-) create mode 120000 workbench/nextjs-turbopack/workflows/3_streams.ts delete mode 100644 workbench/nextjs-turbopack/workflows/streams.ts delete mode 120000 workbench/nitro-v3/workflows/streams.ts create mode 120000 workbench/sveltekit/workflows/3_streams.ts delete mode 120000 workbench/sveltekit/workflows/streams.ts diff --git a/packages/core/e2e/dev-test-factory.ts b/packages/core/e2e/dev-test-factory.ts index d660c899d..31aef9ae8 100644 --- a/packages/core/e2e/dev-test-factory.ts +++ b/packages/core/e2e/dev-test-factory.ts @@ -8,6 +8,8 @@ export interface DevTestConfig { generatedWorkflowPath: string; apiFilePath: string; apiFileImportPath: string; + /** The workflow file to modify for testing HMR. Defaults to 'streams.ts' */ + testWorkflowFile?: string; } export function createDevTests(config: DevTestConfig) { @@ -15,7 +17,7 @@ export function createDevTests(config: DevTestConfig) { const appPath = getWorkbenchAppPath(); const generatedStep = path.join(appPath, config.generatedStepPath); const generatedWorkflow = path.join(appPath, config.generatedWorkflowPath); - + const testWorkflowFile = config.testWorkflowFile ?? '3_streams.ts'; const restoreFiles: Array<{ path: string; content: string }> = []; afterEach(async () => { @@ -29,10 +31,12 @@ export function createDevTests(config: DevTestConfig) { }) ); restoreFiles.length = 0; + // Give the file watcher time to detect the restoration + await new Promise((res) => setTimeout(res, 500)); }); test('should rebuild on workflow change', { timeout: 10_000 }, async () => { - const workflowFile = path.join(appPath, 'workflows', 'streams.ts'); + const workflowFile = path.join(appPath, 'workflows', testWorkflowFile); const content = await fs.readFile(workflowFile, 'utf8'); @@ -60,7 +64,7 @@ export async function myNewWorkflow() { }); test('should rebuild on step change', { timeout: 10_000 }, async () => { - const stepFile = path.join(appPath, 'workflows', 'streams.ts'); + const stepFile = path.join(appPath, 'workflows', testWorkflowFile); const content = await fs.readFile(stepFile, 'utf8'); diff --git a/workbench/nextjs-turbopack/app/api/chat/route.ts b/workbench/nextjs-turbopack/app/api/chat/route.ts index a857b6963..da18db04d 100644 --- a/workbench/nextjs-turbopack/app/api/chat/route.ts +++ b/workbench/nextjs-turbopack/app/api/chat/route.ts @@ -1,6 +1,6 @@ // 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/streams'; +import * as workflows from '@/workflows/3_streams'; export async function POST(_req: Request) { console.log(workflows); diff --git a/workbench/nextjs-turbopack/workflows/3_streams.ts b/workbench/nextjs-turbopack/workflows/3_streams.ts new file mode 120000 index 000000000..6b28c945e --- /dev/null +++ b/workbench/nextjs-turbopack/workflows/3_streams.ts @@ -0,0 +1 @@ +../../example/workflows/3_streams.ts \ No newline at end of file diff --git a/workbench/nextjs-turbopack/workflows/streams.ts b/workbench/nextjs-turbopack/workflows/streams.ts deleted file mode 100644 index 236f0c232..000000000 --- a/workbench/nextjs-turbopack/workflows/streams.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { xxh32 } from '@node-rs/xxhash'; - -// TODO: HMR should work if you have a step only file and -// add a workflow to it -export async function stremWorkflow() { - 'use workflow'; - return 'hi'; -} - -/** - * Durable version of the add function - */ -export async function pumpData(writable: WritableStream) { - 'use step'; - console.log('pumpData', writable); - - const hash = xxh32('hello', 1); - console.log('native hash', hash); - - const writer = writable.getWriter(); - await writer.write('first'); - - await new Promise((resolve) => setTimeout(resolve, 1_000)); - await writer.write('second'); - - return true; -} - -export async function write( - writable: WritableStream, - data: string, - eof = false -) { - 'use step'; - const writer = writable.getWriter(); - await writer.write(new TextEncoder().encode(data)); - if (eof) { - writer.close(); - } -} - -export async function myNewStep() { - 'use step'; - return 'hello world'; -} - -export async function myNewWorkflow() { - 'use workflow'; - return 'hello world'; -} diff --git a/workbench/nitro-v3/routes/api/chat.post.ts b/workbench/nitro-v3/routes/api/chat.post.ts index f1c1bec09..c534d8d4b 100644 --- a/workbench/nitro-v3/routes/api/chat.post.ts +++ b/workbench/nitro-v3/routes/api/chat.post.ts @@ -1,7 +1,7 @@ // 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/streams.js'; +import * as workflows from '../../workflows/3_streams.js'; export default async ({ req }: { req: Request }) => { console.log(workflows); diff --git a/workbench/nitro-v3/workflows/streams.ts b/workbench/nitro-v3/workflows/streams.ts deleted file mode 120000 index b4f749998..000000000 --- a/workbench/nitro-v3/workflows/streams.ts +++ /dev/null @@ -1 +0,0 @@ -../../nextjs-turbopack/workflows/streams.ts \ No newline at end of file diff --git a/workbench/sveltekit/workflows/3_streams.ts b/workbench/sveltekit/workflows/3_streams.ts new file mode 120000 index 000000000..6b28c945e --- /dev/null +++ b/workbench/sveltekit/workflows/3_streams.ts @@ -0,0 +1 @@ +../../example/workflows/3_streams.ts \ No newline at end of file diff --git a/workbench/sveltekit/workflows/streams.ts b/workbench/sveltekit/workflows/streams.ts deleted file mode 120000 index b4f749998..000000000 --- a/workbench/sveltekit/workflows/streams.ts +++ /dev/null @@ -1 +0,0 @@ -../../nextjs-turbopack/workflows/streams.ts \ No newline at end of file From a455b2d681b4eeef6baa6c8a7a4a5e988c354555 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Mon, 3 Nov 2025 23:56:27 -0800 Subject: [PATCH 09/16] fix: sveltekit hmr test file import --- packages/core/e2e/dev-test-factory.ts | 2 -- workbench/sveltekit/src/routes/api/chat/+server.ts | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/core/e2e/dev-test-factory.ts b/packages/core/e2e/dev-test-factory.ts index 31aef9ae8..8ac8b8cbb 100644 --- a/packages/core/e2e/dev-test-factory.ts +++ b/packages/core/e2e/dev-test-factory.ts @@ -31,8 +31,6 @@ export function createDevTests(config: DevTestConfig) { }) ); restoreFiles.length = 0; - // Give the file watcher time to detect the restoration - await new Promise((res) => setTimeout(res, 500)); }); test('should rebuild on workflow change', { timeout: 10_000 }, async () => { diff --git a/workbench/sveltekit/src/routes/api/chat/+server.ts b/workbench/sveltekit/src/routes/api/chat/+server.ts index 99ba5524b..3e2b41d90 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/streams'; +import * as workflows from '../../../../workflows/3_streams'; export const POST: RequestHandler = async ({ request, From 7bfb8d599bf14c59f44d6531ca70c9e444c4258e Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Tue, 4 Nov 2025 00:09:42 -0800 Subject: [PATCH 10/16] fix: nextjs testing file --- .../nextjs-turbopack/workflows/3_streams.ts | 69 ++++++++++++++++++- 1 file changed, 68 insertions(+), 1 deletion(-) mode change 120000 => 100644 workbench/nextjs-turbopack/workflows/3_streams.ts diff --git a/workbench/nextjs-turbopack/workflows/3_streams.ts b/workbench/nextjs-turbopack/workflows/3_streams.ts deleted file mode 120000 index 6b28c945e..000000000 --- a/workbench/nextjs-turbopack/workflows/3_streams.ts +++ /dev/null @@ -1 +0,0 @@ -../../example/workflows/3_streams.ts \ No newline at end of file diff --git a/workbench/nextjs-turbopack/workflows/3_streams.ts b/workbench/nextjs-turbopack/workflows/3_streams.ts new file mode 100644 index 000000000..be193c90d --- /dev/null +++ b/workbench/nextjs-turbopack/workflows/3_streams.ts @@ -0,0 +1,68 @@ +export async function genStream(): Promise> { + 'use step'; + const stream = new ReadableStream({ + async start(controller) { + const encoder = new TextEncoder(); + for (let i = 0; i < 30; i++) { + const chunk = encoder.encode(`${i}\n`); + controller.enqueue(chunk); + console.log(`Enqueued number: ${i}`); + await new Promise((resolve) => setTimeout(resolve, 2500)); + } + controller.close(); + }, + }); + return stream; +} + +export async function consumeStreams( + ...streams: ReadableStream[] +): Promise { + 'use step'; + const parts: Uint8Array[] = []; + + console.log('Consuming streams', streams); + + await Promise.all( + streams.map(async (s, i) => { + const reader = s.getReader(); + while (true) { + const result = await reader.read(); + if (result.done) break; + console.log( + `Received ${result.value.length} bytes from stream ${i}: ${JSON.stringify(new TextDecoder().decode(result.value))}` + ); + parts.push(result.value); + } + }) + ); + + return Buffer.concat(parts).toString('utf8'); +} + +export async function streams() { + 'use workflow'; + + console.log('Streams workflow started'); + + const [s1, s2] = await Promise.all([genStream(), genStream()]); + const result = await consumeStreams(s1, s2); + + console.log(`Streams workflow completed. Result: ${result.slice(0, 100)}`); + + return { + message: 'Streams processed successfully', + dataLength: result.length, + preview: result.slice(0, 100), + }; +} + +export async function myNewWorkflow() { + 'use workflow'; + return 'hello world'; +} + +export async function myNewStep() { + 'use step'; + return 'hello world'; +} From 61d3c313c7495f1c94f31151553a72a971a08664 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Tue, 4 Nov 2025 00:11:36 -0800 Subject: [PATCH 11/16] remove stuff --- workbench/nextjs-turbopack/workflows/3_streams.ts | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/workbench/nextjs-turbopack/workflows/3_streams.ts b/workbench/nextjs-turbopack/workflows/3_streams.ts index be193c90d..afcdd55d5 100644 --- a/workbench/nextjs-turbopack/workflows/3_streams.ts +++ b/workbench/nextjs-turbopack/workflows/3_streams.ts @@ -56,13 +56,3 @@ export async function streams() { preview: result.slice(0, 100), }; } - -export async function myNewWorkflow() { - 'use workflow'; - return 'hello world'; -} - -export async function myNewStep() { - 'use step'; - return 'hello world'; -} From 335605211de97454078d773f141b57f07e3769de Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Tue, 4 Nov 2025 00:20:08 -0800 Subject: [PATCH 12/16] changeset --- .changeset/nine-books-heal.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nine-books-heal.md diff --git a/.changeset/nine-books-heal.md b/.changeset/nine-books-heal.md new file mode 100644 index 000000000..3f6b8049b --- /dev/null +++ b/.changeset/nine-books-heal.md @@ -0,0 +1,5 @@ +--- +"@workflow/sveltekit": patch +--- + +Use hotUpdate for HMR in sveltekit From 124fba60d9b559388b2e03f16d1ec5730e878b58 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Tue, 4 Nov 2025 15:29:00 -0800 Subject: [PATCH 13/16] refactor(tests): expose config through matrix config --- .github/workflows/tests.yml | 38 +++++----------- .../e2e/{dev-test-factory.ts => dev.test.ts} | 40 ++++++++++++++--- packages/core/e2e/next-dev.test.ts | 8 ---- packages/core/e2e/nitro-dev.test.ts | 8 ---- packages/core/e2e/sveltekit-dev.test.ts | 8 ---- scripts/create-test-matrix.mjs | 43 +++++++++++++++++++ 6 files changed, 87 insertions(+), 58 deletions(-) rename packages/core/e2e/{dev-test-factory.ts => dev.test.ts} (75%) delete mode 100644 packages/core/e2e/next-dev.test.ts delete mode 100644 packages/core/e2e/nitro-dev.test.ts delete mode 100644 packages/core/e2e/sveltekit-dev.test.ts diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 18dd7dbe6..1fe6080c0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -178,35 +178,18 @@ jobs: - name: Run Initial Build run: pnpm turbo run build --filter='!./workbench/*' - - name: Resolve symlinks for next dev to work - if: matrix.app.name == 'nextjs-webpack' - run: cd workbench/${{ matrix.app.name }} && ./resolve-symlinks.sh - - - name: Run E2E Tests (Next.js) - if: matrix.app.name != 'nitro' && matrix.app.name != 'vite' && matrix.app.name != 'sveltekit' - run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/next-dev.test.ts && pnpm run test:e2e - env: - APP_NAME: ${{ matrix.app.name }} - DEPLOYMENT_URL: "http://localhost:3000" + - name: Resolve symlinks + run: | + if [ -f "workbench/${{ matrix.app.name }}/resolve-symlinks.sh" ]; then + cd workbench/${{ matrix.app.name }} && ./resolve-symlinks.sh + fi - - name: Run E2E Tests (Nitro) - if: matrix.app.name == 'nitro' - run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/nitro-dev.test.ts && pnpm run test:e2e - env: - APP_NAME: ${{ matrix.app.name }} - DEPLOYMENT_URL: "http://localhost:3000" - - name: Run E2E Tests (Vite) - if: matrix.app.name == 'vite' - run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/e2e.test.ts - env: - APP_NAME: ${{ matrix.app.name }} - DEPLOYMENT_URL: "http://localhost:3000" - - name: Run E2E Tests (SvelteKit) - if: matrix.app.name == 'sveltekit' - run: cd workbench/${{ matrix.app.name }} && pnpm dev & echo "starting tests in 10 seconds" && sleep 10 && pnpm vitest run packages/core/e2e/sveltekit-dev.test.ts && pnpm run test:e2e + - 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 env: APP_NAME: ${{ matrix.app.name }} - DEPLOYMENT_URL: "http://localhost:3000" + DEPLOYMENT_URL: "http://localhost:${{ matrix.app.port }}" + DEV_TEST_CONFIG: ${{ toJSON(matrix.app) }} e2e-local-prod: name: E2E Local Prod Tests (${{ matrix.app.name }} - ${{ matrix.app.canary && 'canary' || 'stable' }}) @@ -299,10 +282,11 @@ jobs: $job = Start-Job -ScriptBlock { Set-Location $using:PWD; pnpm dev } Start-Sleep -Seconds 15 cd ../.. - pnpm vitest run packages/core/e2e/next-dev.test.ts + pnpm vitest run packages/core/e2e/dev.test.ts pnpm run test:e2e Stop-Job $job shell: powershell env: APP_NAME: "nextjs-turbopack" DEPLOYMENT_URL: "http://localhost:3000" + DEV_TEST_CONFIG: '{"generatedStepPath":"app/.well-known/workflow/v1/step/route.js","generatedWorkflowPath":"app/.well-known/workflow/v1/flow/route.js","apiFilePath":"app/api/chat/route.ts","apiFileImportPath":"../../..","port":3000}' diff --git a/packages/core/e2e/dev-test-factory.ts b/packages/core/e2e/dev.test.ts similarity index 75% rename from packages/core/e2e/dev-test-factory.ts rename to packages/core/e2e/dev.test.ts index 8ac8b8cbb..578a01288 100644 --- a/packages/core/e2e/dev-test-factory.ts +++ b/packages/core/e2e/dev.test.ts @@ -8,16 +8,37 @@ export interface DevTestConfig { generatedWorkflowPath: string; apiFilePath: string; apiFileImportPath: string; - /** The workflow file to modify for testing HMR. Defaults to 'streams.ts' */ + /** The workflow file to modify for testing HMR. Defaults to '3_streams.ts' */ testWorkflowFile?: string; } -export function createDevTests(config: DevTestConfig) { +function getConfigFromEnv(): DevTestConfig | null { + const envConfig = process.env.DEV_TEST_CONFIG; + if (envConfig) { + try { + return JSON.parse(envConfig); + } catch (e) { + console.error('Failed to parse DEV_TEST_CONFIG:', e); + } + } + return null; +} + +export function createDevTests(config?: DevTestConfig) { + const finalConfig = config || getConfigFromEnv(); + if (!finalConfig) { + throw new Error( + 'No dev test config provided via parameter or DEV_TEST_CONFIG env var' + ); + } describe('dev e2e', () => { const appPath = getWorkbenchAppPath(); - const generatedStep = path.join(appPath, config.generatedStepPath); - const generatedWorkflow = path.join(appPath, config.generatedWorkflowPath); - const testWorkflowFile = config.testWorkflowFile ?? '3_streams.ts'; + const generatedStep = path.join(appPath, finalConfig.generatedStepPath); + const generatedWorkflow = path.join( + appPath, + finalConfig.generatedWorkflowPath + ); + const testWorkflowFile = finalConfig.testWorkflowFile ?? '3_streams.ts'; const restoreFiles: Array<{ path: string; content: string }> = []; afterEach(async () => { @@ -104,14 +125,14 @@ export async function myNewStep() { ` ); restoreFiles.push({ path: workflowFile, content: '' }); - const apiFile = path.join(appPath, config.apiFilePath); + const apiFile = path.join(appPath, finalConfig.apiFilePath); const apiFileContent = await fs.readFile(apiFile, 'utf8'); restoreFiles.push({ path: apiFile, content: apiFileContent }); await fs.writeFile( apiFile, - `import '${config.apiFileImportPath}/workflows/new-workflow'; + `import '${finalConfig.apiFileImportPath}/workflows/new-workflow'; ${apiFileContent}` ); @@ -131,3 +152,8 @@ ${apiFileContent}` ); }); } + +// Run tests with environment-based config if this file is executed directly +if (process.env.DEV_TEST_CONFIG) { + createDevTests(); +} diff --git a/packages/core/e2e/next-dev.test.ts b/packages/core/e2e/next-dev.test.ts deleted file mode 100644 index 77a85022c..000000000 --- a/packages/core/e2e/next-dev.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createDevTests } from './dev-test-factory'; - -createDevTests({ - generatedStepPath: 'app/.well-known/workflow/v1/step/route.js', - generatedWorkflowPath: 'app/.well-known/workflow/v1/flow/route.js', - apiFilePath: 'app/api/chat/route.ts', - apiFileImportPath: '../../..', -}); diff --git a/packages/core/e2e/nitro-dev.test.ts b/packages/core/e2e/nitro-dev.test.ts deleted file mode 100644 index 09141e5a7..000000000 --- a/packages/core/e2e/nitro-dev.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createDevTests } from './dev-test-factory'; - -createDevTests({ - generatedStepPath: '.nitro/workflow/steps.mjs', - generatedWorkflowPath: '.nitro/workflow/workflows.mjs', - apiFilePath: 'routes/api/chat.post.ts', - apiFileImportPath: '../..', -}); diff --git a/packages/core/e2e/sveltekit-dev.test.ts b/packages/core/e2e/sveltekit-dev.test.ts deleted file mode 100644 index e005bcd69..000000000 --- a/packages/core/e2e/sveltekit-dev.test.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createDevTests } from './dev-test-factory'; - -createDevTests({ - generatedStepPath: 'src/routes/.well-known/workflow/v1/step/+server.js', - generatedWorkflowPath: 'src/routes/.well-known/workflow/v1/flow/+server.js', - apiFilePath: 'src/routes/api/chat/+server.ts', - apiFileImportPath: '../../../..', -}); diff --git a/scripts/create-test-matrix.mjs b/scripts/create-test-matrix.mjs index 007fd7598..7489c685e 100644 --- a/scripts/create-test-matrix.mjs +++ b/scripts/create-test-matrix.mjs @@ -1,12 +1,53 @@ +// Framework-specific dev test configurations +const DEV_TEST_CONFIGS = { + 'nextjs-turbopack': { + generatedStepPath: 'app/.well-known/workflow/v1/step/route.js', + generatedWorkflowPath: 'app/.well-known/workflow/v1/flow/route.js', + apiFilePath: 'app/api/chat/route.ts', + apiFileImportPath: '../../..', + port: 3000, + }, + 'nextjs-webpack': { + generatedStepPath: 'app/.well-known/workflow/v1/step/route.js', + generatedWorkflowPath: 'app/.well-known/workflow/v1/flow/route.js', + apiFilePath: 'app/api/chat/route.ts', + apiFileImportPath: '../../..', + port: 3000, + }, + nitro: { + generatedStepPath: '.nitro/workflow/steps.mjs', + generatedWorkflowPath: '.nitro/workflow/workflows.mjs', + apiFilePath: 'routes/api/chat.post.ts', + apiFileImportPath: '../..', + port: 3000, + }, + sveltekit: { + generatedStepPath: 'src/routes/.well-known/workflow/v1/step/+server.js', + generatedWorkflowPath: 'src/routes/.well-known/workflow/v1/flow/+server.js', + apiFilePath: 'src/routes/api/chat/+server.ts', + apiFileImportPath: '../../../..', + port: 3000, + }, + vite: { + generatedStepPath: 'dist/workflow/steps.mjs', + generatedWorkflowPath: 'dist/workflow/workflows.mjs', + apiFilePath: 'src/main.ts', + apiFileImportPath: '..', + port: 3000, + }, +}; + const matrix = { app: [ { name: 'nextjs-turbopack', project: 'example-nextjs-workflow-turbopack', + ...DEV_TEST_CONFIGS['nextjs-turbopack'], }, { name: 'nextjs-webpack', project: 'example-nextjs-workflow-webpack', + ...DEV_TEST_CONFIGS['nextjs-webpack'], }, ], }; @@ -24,11 +65,13 @@ if (process.env.GITHUB_REF === 'refs/heads/main') { matrix.app.push({ name: 'nitro', project: 'workbench-nitro-workflow', + ...DEV_TEST_CONFIGS.nitro, }); matrix.app.push({ name: 'sveltekit', project: 'workbench-sveltekit-workflow', + ...DEV_TEST_CONFIGS.sveltekit, }); console.log(JSON.stringify(matrix)); From 27e45bf7f739f7d961f2ea63bcd5f7376b4f4357 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Tue, 4 Nov 2025 15:52:08 -0800 Subject: [PATCH 14/16] fix: add symlink for nextjs turbopack --- .../nextjs-turbopack/workflows/3_streams.ts | 59 +------------------ 1 file changed, 1 insertion(+), 58 deletions(-) mode change 100644 => 120000 workbench/nextjs-turbopack/workflows/3_streams.ts diff --git a/workbench/nextjs-turbopack/workflows/3_streams.ts b/workbench/nextjs-turbopack/workflows/3_streams.ts deleted file mode 100644 index afcdd55d5..000000000 --- a/workbench/nextjs-turbopack/workflows/3_streams.ts +++ /dev/null @@ -1,58 +0,0 @@ -export async function genStream(): Promise> { - 'use step'; - const stream = new ReadableStream({ - async start(controller) { - const encoder = new TextEncoder(); - for (let i = 0; i < 30; i++) { - const chunk = encoder.encode(`${i}\n`); - controller.enqueue(chunk); - console.log(`Enqueued number: ${i}`); - await new Promise((resolve) => setTimeout(resolve, 2500)); - } - controller.close(); - }, - }); - return stream; -} - -export async function consumeStreams( - ...streams: ReadableStream[] -): Promise { - 'use step'; - const parts: Uint8Array[] = []; - - console.log('Consuming streams', streams); - - await Promise.all( - streams.map(async (s, i) => { - const reader = s.getReader(); - while (true) { - const result = await reader.read(); - if (result.done) break; - console.log( - `Received ${result.value.length} bytes from stream ${i}: ${JSON.stringify(new TextDecoder().decode(result.value))}` - ); - parts.push(result.value); - } - }) - ); - - return Buffer.concat(parts).toString('utf8'); -} - -export async function streams() { - 'use workflow'; - - console.log('Streams workflow started'); - - const [s1, s2] = await Promise.all([genStream(), genStream()]); - const result = await consumeStreams(s1, s2); - - console.log(`Streams workflow completed. Result: ${result.slice(0, 100)}`); - - return { - message: 'Streams processed successfully', - dataLength: result.length, - preview: result.slice(0, 100), - }; -} diff --git a/workbench/nextjs-turbopack/workflows/3_streams.ts b/workbench/nextjs-turbopack/workflows/3_streams.ts new file mode 120000 index 000000000..6b28c945e --- /dev/null +++ b/workbench/nextjs-turbopack/workflows/3_streams.ts @@ -0,0 +1 @@ +../../example/workflows/3_streams.ts \ No newline at end of file From 8a94c71b072dba3d09dc5976c79685ad0775fde5 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Tue, 4 Nov 2025 16:09:01 -0800 Subject: [PATCH 15/16] add resolve symlinks script --- .../nextjs-turbopack/resolve-symlinks.sh | 43 +++++++++++++++++++ workbench/nitro-v3/resolve-symlinks.sh | 43 +++++++++++++++++++ workbench/sveltekit/resolve-symlinks.sh | 43 +++++++++++++++++++ 3 files changed, 129 insertions(+) create mode 100755 workbench/nextjs-turbopack/resolve-symlinks.sh create mode 100755 workbench/nitro-v3/resolve-symlinks.sh create mode 100755 workbench/sveltekit/resolve-symlinks.sh diff --git a/workbench/nextjs-turbopack/resolve-symlinks.sh b/workbench/nextjs-turbopack/resolve-symlinks.sh new file mode 100755 index 000000000..425bd7296 --- /dev/null +++ b/workbench/nextjs-turbopack/resolve-symlinks.sh @@ -0,0 +1,43 @@ +#!/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/resolve-symlinks.sh b/workbench/nitro-v3/resolve-symlinks.sh new file mode 100755 index 000000000..425bd7296 --- /dev/null +++ b/workbench/nitro-v3/resolve-symlinks.sh @@ -0,0 +1,43 @@ +#!/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/resolve-symlinks.sh b/workbench/sveltekit/resolve-symlinks.sh new file mode 100755 index 000000000..425bd7296 --- /dev/null +++ b/workbench/sveltekit/resolve-symlinks.sh @@ -0,0 +1,43 @@ +#!/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!" From cd1858ec8ce819460e0192ebac58d73712799933 Mon Sep 17 00:00:00 2001 From: Adrian Lam Date: Tue, 4 Nov 2025 16:36:15 -0800 Subject: [PATCH 16/16] fix: nextjs-webpack resolve symlinks script --- workbench/nextjs-webpack/resolve-symlinks.sh | 48 +++++++++++++++++++- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/workbench/nextjs-webpack/resolve-symlinks.sh b/workbench/nextjs-webpack/resolve-symlinks.sh index 425bd7296..6c8a7acbb 100755 --- a/workbench/nextjs-webpack/resolve-symlinks.sh +++ b/workbench/nextjs-webpack/resolve-symlinks.sh @@ -11,10 +11,54 @@ if [ -z "$CI" ]; then exit 1 fi -echo "Resolving all symlinks in current directory..." +echo "Resolving symlinked files in workflows directory..." -# Find all symlinks in current directory (including nested ones), excluding gitignored files +# Special handling for workflows directory if it's a symlink +if [ -L "workflows" ]; then + workflows_target=$(readlink "workflows") + # Resolve relative path + if [[ "$workflows_target" != /* ]]; then + workflows_target="$PWD/$workflows_target" + fi + + echo "Workflows directory is a symlink to: $workflows_target" + + # Remove the workflows symlink + rm "workflows" + + # Create workflows as a real directory + mkdir -p "workflows" + + # Copy all files from the target, resolving any symlinks in the process + if [ -d "$workflows_target" ]; then + for file in "$workflows_target"/*; do + filename=$(basename "$file") + if [ -L "$file" ]; then + # If it's a symlink, resolve it + file_target=$(readlink "$file") + if [[ "$file_target" != /* ]]; then + file_target="$(dirname "$file")/$file_target" + fi + echo " Copying and resolving: $filename -> $file_target" + cp "$file_target" "workflows/$filename" + else + # If it's a regular file, just copy it + echo " Copying: $filename" + cp "$file" "workflows/$filename" + fi + done + fi +fi + +echo "Resolving other symlinks..." + +# Find all other symlinks (excluding workflows which we already handled) git ls-files -z --cached --others --exclude-standard | xargs -0 -I {} sh -c 'test -L "{}" && echo "{}"' | while read -r symlink; do + # Skip workflows directory as we already handled it + if [ "$symlink" = "workflows" ]; then + continue + fi + # Get the target of the symlink target=$(readlink "$symlink")