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 18262b2c51..d31470d3a3 100644 --- a/packages/next/src/index.ts +++ b/packages/next/src/index.ts @@ -1,6 +1,6 @@ import { copyFileSync, mkdirSync, statSync } from 'node:fs'; import { copyFile, mkdir, readFile } from 'node:fs/promises'; -import { dirname, isAbsolute, join } from 'node:path'; +import { dirname, isAbsolute, join, resolve } from 'node:path'; import type { NextConfig } from 'next'; import semver from 'semver'; import { getNextBuilder } from './builder.js'; @@ -16,7 +16,6 @@ const VERCEL_WORLD_SERVER_EXTERNAL_PACKAGES = [ VERCEL_WORLD_PACKAGE, ...VERCEL_WORLD_DEPENDENCY_PACKAGES, ]; - const useWorkflowPattern = /^\s*(['"])use workflow\1;?\s*$/m; const useStepPattern = /^\s*(['"])use step\1;?\s*$/m; const workflowSerdeImportPattern = /from\s+(['"])@workflow\/serde\1/; @@ -232,6 +231,74 @@ function fileExists(path: string): boolean { } } +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; + } +} + function getWorkflowManifestCopyPaths({ projectDir, distDir, @@ -442,7 +509,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'; @@ -474,9 +543,9 @@ export function withWorkflow( 'jsx', 'js', ], - projectRoot: nextConfig.outputFileTracingRoot, - moduleSpecifierRoot: process.cwd(), - workingDir: process.cwd(), + projectRoot, + moduleSpecifierRoot: workingDir, + workingDir, distDir, diagnosticsDir: `${distDir}/diagnostics`, buildTarget: 'next',