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; }