diff --git a/.changeset/next-root-detection.md b/.changeset/next-root-detection.md new file mode 100644 index 0000000000..e16f8d1d37 --- /dev/null +++ b/.changeset/next-root-detection.md @@ -0,0 +1,5 @@ +--- +"@workflow/next": patch +--- + +Derive the workflow builder project root from Next.js workspace root configuration. diff --git a/packages/next/src/index.ts b/packages/next/src/index.ts index 82ace59902..6b0e885914 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -1,4 +1,6 @@ +import { statSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; +import { dirname, isAbsolute, join, resolve } from 'node:path'; import type { NextConfig } from 'next'; import semver from 'semver'; import { getNextBuilder } from './builder.js'; @@ -193,6 +195,82 @@ function resolveNextVersion(workingDir: string): string { } } +function fileExists(path: string): boolean { + try { + return statSync(path).isFile(); + } catch { + return false; + } +} + +function findRootFile(names: string[], workingDir: string): string | undefined { + let current = resolve(workingDir); + + while (true) { + for (const name of names) { + const file = join(current, name); + if (fileExists(file)) { + return file; + } + } + + const parent = dirname(current); + if (parent === current) { + return undefined; + } + current = parent; + } +} + +function findNextRootFile(workingDir: string): string | undefined { + return ( + findRootFile(['pnpm-workspace.yaml'], workingDir) ?? + findRootFile( + [ + 'pnpm-lock.yaml', + 'package-lock.json', + 'yarn.lock', + 'bun.lock', + 'bun.lockb', + ], + workingDir + ) + ); +} + +function resolveNextProjectRoot( + nextConfig: NextConfig, + workingDir: string +): string { + const configuredRoot = + nextConfig.outputFileTracingRoot ?? nextConfig.turbopack?.root; + + if (configuredRoot) { + return isAbsolute(configuredRoot) + ? configuredRoot + : resolve(workingDir, configuredRoot); + } + + let rootFile = findNextRootFile(workingDir); + if (!rootFile) { + return workingDir; + } + + while (true) { + const currentDir = dirname(rootFile); + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + return currentDir; + } + + const parentRootFile = findNextRootFile(parentDir); + if (!parentRootFile) { + return currentDir; + } + rootFile = parentRootFile; + } +} + export function withWorkflow( nextConfigOrFn: | NextConfig @@ -288,7 +366,9 @@ export function withWorkflow( nextConfig.turbopack.rules = {}; } const existingRules = nextConfig.turbopack.rules as any; - const nextVersion = resolveNextVersion(process.cwd()); + const workingDir = process.cwd(); + const nextVersion = resolveNextVersion(workingDir); + const projectRoot = resolveNextProjectRoot(nextConfig, workingDir); const supportsTurboCondition = semver.gte(nextVersion, 'v16.0.0'); const shouldWatch = process.env.NODE_ENV === 'development'; @@ -308,9 +388,9 @@ export function withWorkflow( 'jsx', 'js', ], - projectRoot: nextConfig.outputFileTracingRoot, - moduleSpecifierRoot: process.cwd(), - workingDir: process.cwd(), + projectRoot, + moduleSpecifierRoot: workingDir, + workingDir, distDir: nextConfig.distDir || '.next', buildTarget: 'next', workflowsBundlePath: '', // not used in base