From 7e4c85bc5c8330f9477eed622e7c5ff8c86b9398 Mon Sep 17 00:00:00 2001 From: Robert Vollmer Date: Sat, 21 Feb 2026 06:49:22 +0100 Subject: [PATCH] feat(next): Allow specifying workflow directories This brings back the option introduced in #843 (and removed in #961), but with a slightly different meaning. Instead of specifying directories of pages whiuch call server actions which trigger workflows, the option is now meant to directly specify the directory with the workflow files. That removes two layers of indirection, imports way less files and therefore really improves performance as the original PR intended (~15s -> ~2.5s on a medium-sized app). --- .changeset/thick-months-wave.md | 5 +++ .../workflow-next/with-workflow.mdx | 39 +++++++++++++++++++ docs/content/docs/getting-started/next.mdx | 39 +++++++++++++++++++ packages/builders/src/types.ts | 1 + .../cli/src/lib/config/workflow-config.ts | 1 + packages/next/src/builder-deferred.ts | 6 +++ packages/next/src/builder-eager.ts | 6 +++ packages/next/src/index.ts | 4 +- packages/nitro/src/builders.ts | 2 +- 9 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 .changeset/thick-months-wave.md diff --git a/.changeset/thick-months-wave.md b/.changeset/thick-months-wave.md new file mode 100644 index 0000000000..f7bc4cf11b --- /dev/null +++ b/.changeset/thick-months-wave.md @@ -0,0 +1,5 @@ +--- +"@workflow/next": patch +--- + +Allow specifying workflow directories diff --git a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx index d42adc333a..345cc5e0c7 100644 --- a/docs/content/docs/api-reference/workflow-next/with-workflow.mdx +++ b/docs/content/docs/api-reference/workflow-next/with-workflow.mdx @@ -54,3 +54,42 @@ export default async function config( return nextConfig; } ``` + +## Configuration + +The second argument to `withWorkflow` accepts the following options: + +### `workflows.dirs` + +Directories containing files with `use workflow` directives. + +By default, `withWorkflow` uses Next.js entrypoint directories (`pages`, `app`, `src/pages`, `src/app`) as starting points and traces their imports to discover workflow files. If your workflows live in dedicated directories, use this option to point directly at them — avoiding the need to crawl the entire app's import tree. + +- **Type:** `string[]` + +```typescript title="next.config.ts" lineNumbers +import { withWorkflow } from "workflow/next"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default withWorkflow(nextConfig, { + workflows: { + dirs: ['workflows', 'src/workflows'], // [!code highlight] + }, +}); +``` + +### `workflows.local.port` + +Port for the local workflow server during development. + +- **Type:** `number` +- **Default:** Uses the `PORT` environment variable + +### `workflows.local.dataDir` + +Directory for storing local workflow data during development. + +- **Type:** `string` +- **Default:** `'.next/workflow-data'` diff --git a/docs/content/docs/getting-started/next.mdx b/docs/content/docs/getting-started/next.mdx index 828a7f6e27..1acbd94a3d 100644 --- a/docs/content/docs/getting-started/next.mdx +++ b/docs/content/docs/getting-started/next.mdx @@ -264,6 +264,45 @@ Check the [Deploying](/docs/deploying) section to learn how your workflows can b ## Troubleshooting +### Out of Memory (OOM) during build + +When using `withWorkflow()` in a large Next.js application, the "Discovering workflow directives" phase may consume excessive memory, causing builds to fail with OOM errors on standard build machines (e.g., 16 GB RAM). + +**Solution:** Use the `workflows.dirs` option to point directly at the directories containing your workflow files. By default, `withWorkflow` uses entrypoint directories (`pages`, `app`, `src/pages`, `src/app`) as starting points and traces their imports to discover workflows, which in large applications can involve crawling thousands of files. + +If your workflows live in a dedicated directory, configure `dirs` to only scan that directory: + +```typescript title="next.config.ts" lineNumbers +import { withWorkflow } from "workflow/next"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default withWorkflow(nextConfig, { + workflows: { + // Only scan the workflows directory instead of the entire app + dirs: ['workflows'], // [!code highlight] + }, +}); +``` + +If your workflows are spread across multiple directories: + +```typescript title="next.config.ts" lineNumbers +import { withWorkflow } from "workflow/next"; +import type { NextConfig } from "next"; + +const nextConfig: NextConfig = {}; + +export default withWorkflow(nextConfig, { + workflows: { + dirs: ['workflows', 'src/background-jobs'], // [!code highlight] + }, +}); +``` + +This significantly reduces memory usage and build times by avoiding scanning unrelated application code. + ### Next.js 16.1+ compatibility If you see this error when upgrading to Next.js 16.1 or later: diff --git a/packages/builders/src/types.ts b/packages/builders/src/types.ts index 070e881c66..e0baed33e3 100644 --- a/packages/builders/src/types.ts +++ b/packages/builders/src/types.ts @@ -70,6 +70,7 @@ export interface VercelBuildOutputConfig extends BaseWorkflowConfig { */ export interface NextConfig extends BaseWorkflowConfig { buildTarget: 'next'; + dirsAreEntrypoints: boolean; // Next.js builder computes paths dynamically, so these are not used stepsBundlePath: string; workflowsBundlePath: string; diff --git a/packages/cli/src/lib/config/workflow-config.ts b/packages/cli/src/lib/config/workflow-config.ts index e8e8636515..63cde6f70f 100644 --- a/packages/cli/src/lib/config/workflow-config.ts +++ b/packages/cli/src/lib/config/workflow-config.ts @@ -24,6 +24,7 @@ export const getWorkflowConfig = ( ) => { const config: WorkflowConfig = { dirs: ['./workflows'], + dirsAreEntrypoints: false, workingDir: resolveObservabilityCwd(), buildTarget: buildTarget as BuildTarget, stepsBundlePath: './.well-known/workflow/v1/step.js', diff --git a/packages/next/src/builder-deferred.ts b/packages/next/src/builder-deferred.ts index 39c1358638..c61529214e 100644 --- a/packages/next/src/builder-deferred.ts +++ b/packages/next/src/builder-deferred.ts @@ -1,3 +1,4 @@ +import type { NextConfig } from '@workflow/builders'; import { createHash } from 'node:crypto'; import { constants, existsSync, realpathSync } from 'node:fs'; import { @@ -65,6 +66,8 @@ export async function getNextBuilderDeferred() { )) as typeof import('@workflow/builders'); class NextDeferredBuilder extends BaseBuilderClass { + protected declare config: NextConfig; + private socketIO?: SocketIO; private readonly discoveredWorkflowFiles = new Set(); private readonly discoveredStepFiles = new Set(); @@ -953,6 +956,9 @@ export async function getNextBuilderDeferred() { protected async getInputFiles(): Promise { const inputFiles = await super.getInputFiles(); + if (!this.config.dirsAreEntrypoints) { + return inputFiles; + } return inputFiles.filter((item) => { // Match App Router entrypoints: route.ts, page.ts, layout.ts in app/ or src/app/ directories // Matches: /app/page.ts, /app/dashboard/page.ts, /src/app/route.ts, etc. diff --git a/packages/next/src/builder-eager.ts b/packages/next/src/builder-eager.ts index c46eb03e62..7b7b1081f6 100644 --- a/packages/next/src/builder-eager.ts +++ b/packages/next/src/builder-eager.ts @@ -1,3 +1,4 @@ +import type { NextConfig } from '@workflow/builders'; import { constants } from 'node:fs'; import { access, copyFile, mkdir, stat, writeFile } from 'node:fs/promises'; import { extname, join, resolve } from 'node:path'; @@ -23,6 +24,8 @@ export async function getNextBuilderEager() { )) as typeof import('@workflow/builders'); class NextBuilder extends BaseBuilderClass { + protected declare config: NextConfig; + async build() { const outputDir = await this.findAppDirectory(); const workflowGeneratedDir = join(outputDir, '.well-known/workflow/v1'); @@ -390,6 +393,9 @@ export async function getNextBuilderEager() { protected async getInputFiles(): Promise { const inputFiles = await super.getInputFiles(); + if (!this.config.dirsAreEntrypoints) { + return inputFiles; + } return inputFiles.filter((item) => { // Match App Router entrypoints: route.ts, page.ts, layout.ts in app/ or src/app/ directories // Matches: /app/page.ts, /app/dashboard/page.ts, /src/app/route.ts, etc. diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index 13c5b9f26a..0c5ec4eaf4 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -18,6 +18,7 @@ export function withWorkflow( }: { workflows?: { lazyDiscovery?: boolean; + dirs?: string[]; local?: { port?: number; dataDir?: string; @@ -87,7 +88,8 @@ export function withWorkflow( return new NextBuilder({ watch: shouldWatch, // discover workflows from pages/app entries - dirs: ['pages', 'app', 'src/pages', 'src/app'], + dirs: workflows?.dirs || ['pages', 'app', 'src/pages', 'src/app'], + dirsAreEntrypoints: !workflows?.dirs, workingDir: process.cwd(), distDir: nextConfig.distDir || '.next', buildTarget: 'next', diff --git a/packages/nitro/src/builders.ts b/packages/nitro/src/builders.ts index fa67ec5cac..76d23c8e70 100644 --- a/packages/nitro/src/builders.ts +++ b/packages/nitro/src/builders.ts @@ -41,7 +41,7 @@ export class LocalBuilder extends BaseBuilder { watch: nitro.options.dev, dirs: ['.'], // Different apps that use nitro have different directories }), - buildTarget: 'next', // Placeholder, not actually used + buildTarget: 'standalone', // Placeholder, not actually used }); this.#outDir = outDir; }