Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/next-diagnostics-dist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workflow/builders": patch
"@workflow/next": patch
---

Write Next.js workflow diagnostics manifests inside the Next.js dist directory and only use `.vercel/output/diagnostics` for the Vercel Build Output API builder.
31 changes: 20 additions & 11 deletions packages/builders/src/base-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
import { mkdir, readFile, realpath, rename, writeFile } from 'node:fs/promises';
import { basename, dirname, join, relative, resolve } from 'node:path';
import { promisify } from 'node:util';
import { pluralize, usesVercelWorld } from '@workflow/utils';
import { pluralize } from '@workflow/utils';
import chalk from 'chalk';
import enhancedResolveOriginal from 'enhanced-resolve';
import * as esbuild from 'esbuild';
Expand Down Expand Up @@ -1320,13 +1320,23 @@ export const OPTIONS = handler;`;
}

/**
* Whether diagnostics artifacts should be emitted to Vercel output.
* This is enabled when the resolved world target is Vercel.
* Resolves the workflow manifest diagnostics path, when this builder should
* emit one.
*/
protected get shouldEmitVercelDiagnostics(): boolean {
return (
usesVercelWorld() || this.config.buildTarget === 'vercel-build-output-api'
);
protected getDiagnosticsManifestPath(): string | undefined {
if (this.config.diagnosticsDir) {
return resolve(
this.config.workingDir,
this.config.diagnosticsDir,
'workflows-manifest.json'
);
}

if (this.config.buildTarget === 'vercel-build-output-api') {
return this.resolvePath(
'.vercel/output/diagnostics/workflows-manifest.json'
);
Comment on lines +1336 to +1338
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should only ever write to this dir, if it's a prebuilt build. Is it what the if condition checks?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why would we only want for prebuilt we always want this included in diagnostics for debugging same as the other stuff we output to the diagnostics for Next.js builds.

}
}

/**
Expand Down Expand Up @@ -1362,10 +1372,9 @@ export const OPTIONS = handler;`;

await mkdir(manifestDir, { recursive: true });
await writeFile(join(manifestDir, 'manifest.json'), manifestJson);
if (this.shouldEmitVercelDiagnostics) {
const diagnosticsManifestPath = this.resolvePath(
'.vercel/output/diagnostics/workflows-manifest.json'
);

const diagnosticsManifestPath = this.getDiagnosticsManifestPath();
if (diagnosticsManifestPath) {
await this.ensureDirectory(diagnosticsManifestPath);
await writeFile(diagnosticsManifestPath, manifestJson);
}
Expand Down
67 changes: 65 additions & 2 deletions packages/builders/src/get-input-files.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { tmpdir } from 'node:os';
import { dirname, join } from 'node:path';
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { BaseBuilder } from './base-builder.js';
import type { StandaloneConfig } from './types.js';
import type { StandaloneConfig, VercelBuildOutputConfig } from './types.js';

/**
* Minimal subclass to expose the protected `getInputFiles()` for testing.
Expand All @@ -23,6 +23,10 @@ class TestBuilder extends BaseBuilder {
public getInputFiles(): Promise<string[]> {
return super.getInputFiles();
}

public getDiagnosticsManifestPath(): string | undefined {
return super.getDiagnosticsManifestPath();
}
}

// Resolve symlinks in tmpdir to avoid macOS /var -> /private/var issues
Expand All @@ -44,14 +48,19 @@ function writeFile(dir: string, relativePath: string, content = ''): string {
return fullPath;
}

function createBuilder(workingDir: string, dirs: string[]): TestBuilder {
function createBuilder(
workingDir: string,
dirs: string[],
configOverrides: Partial<StandaloneConfig> = {}
): TestBuilder {
const config: StandaloneConfig = {
buildTarget: 'standalone',
workingDir,
dirs,
stepsBundlePath: join(workingDir, 'steps.js'),
workflowsBundlePath: join(workingDir, 'workflows.js'),
webhookBundlePath: join(workingDir, 'webhook.js'),
...configOverrides,
};
return new TestBuilder(config);
}
Expand Down Expand Up @@ -172,3 +181,57 @@ describe('getInputFiles', () => {
expect(files).toContain(normalize(join(srcDir, '.api/config.cjs')));
});
});

describe('getDiagnosticsManifestPath', () => {
let testRoot: string;

beforeEach(() => {
testRoot = mkdtempSync(join(realTmpdir, 'diagnostics-path-'));
});

afterEach(() => {
rmSync(testRoot, { recursive: true, force: true });
});

it('uses an explicit diagnosticsDir when configured', () => {
const builder = createBuilder(testRoot, ['src'], {
diagnosticsDir: '.next/diagnostics',
});

expect(builder.getDiagnosticsManifestPath()).toBe(
join(testRoot, '.next/diagnostics/workflows-manifest.json')
);
});

it('does not emit Vercel diagnostics for non-Vercel builder targets', () => {
const previousTargetWorld = process.env.WORKFLOW_TARGET_WORLD;
process.env.WORKFLOW_TARGET_WORLD = 'vercel';
try {
const builder = createBuilder(testRoot, ['src']);

expect(builder.getDiagnosticsManifestPath()).toBeUndefined();
} finally {
if (previousTargetWorld === undefined) {
delete process.env.WORKFLOW_TARGET_WORLD;
} else {
process.env.WORKFLOW_TARGET_WORLD = previousTargetWorld;
}
}
});

it('falls back to Vercel output diagnostics for the Vercel builder', () => {
const config: VercelBuildOutputConfig = {
buildTarget: 'vercel-build-output-api',
workingDir: testRoot,
dirs: ['src'],
stepsBundlePath: join(testRoot, 'steps.js'),
workflowsBundlePath: join(testRoot, 'workflows.js'),
webhookBundlePath: join(testRoot, 'webhook.js'),
};
const builder = new TestBuilder(config);

expect(builder.getDiagnosticsManifestPath()).toBe(
join(testRoot, '.vercel/output/diagnostics/workflows-manifest.json')
);
});
});
8 changes: 8 additions & 0 deletions packages/builders/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,14 @@ interface BaseWorkflowConfig {
// Optional prefix for debug files (e.g., "_" for Astro to ignore them)
debugFilePrefix?: string;

// Optional directory where diagnostics artifacts should be written.
// The workflow manifest is written to workflows-manifest.json inside this dir.
diagnosticsDir?: string;

// Optional framework output directory, used by builders that mirror framework
// artifact locations.
distDir?: string;

// Suppress informational logs emitted by createWorkflowsBundle()
// (e.g. intermediate/final workflow bundle timing logs).
suppressCreateWorkflowsBundleLogs?: boolean;
Expand Down
16 changes: 11 additions & 5 deletions packages/core/e2e/local-build.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { spawn } from 'node:child_process';
import fs from 'node:fs/promises';
import path from 'node:path';
import { describe, expect, test } from 'vitest';
import { usesVercelWorld } from '../../utils/src/world-target';
import { getWorkbenchAppPath } from './utils';

interface CommandResult {
Expand Down Expand Up @@ -83,6 +82,12 @@ const ESM_STEP_BUNDLE_PROJECTS: Record<string, string> = {
'.vercel/output/functions/.well-known/workflow/v1/step.func/index.mjs',
};

const DIAGNOSTICS_MANIFEST_PATHS: Record<string, string> = {
example: '.vercel/output/diagnostics/workflows-manifest.json',
'nextjs-webpack': '.next/diagnostics/workflows-manifest.json',
'nextjs-turbopack': '.next/diagnostics/workflows-manifest.json',
};
Comment thread
ijjk marked this conversation as resolved.

const DEFERRED_BUILD_MODE_PROJECTS = new Set([
'nextjs-webpack',
'nextjs-turbopack',
Expand Down Expand Up @@ -128,12 +133,13 @@ describe.each([
}
}

if (usesVercelWorld()) {
const diagnosticsManifestPath = path.join(
const diagnosticsManifestPath = DIAGNOSTICS_MANIFEST_PATHS[project];
if (diagnosticsManifestPath) {
const resolvedDiagnosticsManifestPath = path.join(
getWorkbenchAppPath(project),
'.vercel/output/diagnostics/workflows-manifest.json'
diagnosticsManifestPath
);
await fs.access(diagnosticsManifestPath);
await fs.access(resolvedDiagnosticsManifestPath);
}

// Verify ESM step bundles use native import.meta (no CJS polyfill needed)
Expand Down
2 changes: 1 addition & 1 deletion packages/next/src/builder-deferred.ts
Original file line number Diff line number Diff line change
Expand Up @@ -786,7 +786,7 @@ export async function getNextBuilderDeferred() {
}

private getDistDir(): string {
return (this.config as { distDir?: string }).distDir || '.next';
return this.config.distDir || '.next';
}

private getWorkflowsCacheFilePath(): string {
Expand Down
30 changes: 29 additions & 1 deletion packages/next/src/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ const loaderStubPath = join(
);
const hadLoaderStub = existsSync(loaderStubPath);

describe('withWorkflow outputFileTracingRoot', () => {
describe('withWorkflow builder config', () => {
const originalEnv = {
PORT: process.env.PORT,
VERCEL_DEPLOYMENT_ID: process.env.VERCEL_DEPLOYMENT_ID,
Expand Down Expand Up @@ -109,4 +109,32 @@ describe('withWorkflow outputFileTracingRoot', () => {
workingDir: process.cwd(),
});
});

it('configures diagnostics inside the default Next.js dist dir', async () => {
const config = withWorkflow({});

await config('phase-production-build', {
defaultConfig: {},
});

expect(builderConfigs[0]).toMatchObject({
distDir: '.next',
diagnosticsDir: '.next/diagnostics',
});
});

it('configures diagnostics inside a custom Next.js dist dir', async () => {
const config = withWorkflow({
distDir: 'build-output',
});

await config('phase-production-build', {
defaultConfig: {},
});

expect(builderConfigs[0]).toMatchObject({
distDir: 'build-output',
diagnosticsDir: 'build-output/diagnostics',
});
});
});
4 changes: 3 additions & 1 deletion packages/next/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ export function withWorkflow(
supportsTurboCondition && !useDeferredBuilder;
const shouldWatch = process.env.NODE_ENV === 'development';
let workflowBuilderPromise: Promise<any> | undefined;
const distDir = nextConfig.distDir || '.next';

const getWorkflowBuilder = async () => {
if (!workflowBuilderPromise) {
Expand All @@ -126,7 +127,8 @@ export function withWorkflow(
dirs: ['pages', 'app', 'src/pages', 'src/app'],
projectRoot: nextConfig.outputFileTracingRoot,
workingDir: process.cwd(),
distDir: nextConfig.distDir || '.next',
distDir,
diagnosticsDir: `${distDir}/diagnostics`,
buildTarget: 'next',
workflowsBundlePath: '', // not used in base
stepsBundlePath: '', // not used in base
Expand Down
Loading