Skip to content
Open
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
5 changes: 5 additions & 0 deletions .changeset/add-type-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@workflow/builders": patch
---

Add type safety for builder configurations with discriminated unions
8 changes: 7 additions & 1 deletion packages/builders/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
export { BaseBuilder } from './base-builder.js';
export { StandaloneBuilder } from './standalone.js';
export { VercelBuildOutputAPIBuilder } from './vercel-build-output-api.js';
export type { WorkflowConfig, BuildTarget } from './types.js';
export type {
WorkflowConfig,
BuildTarget,
StandaloneConfig,
VercelBuildOutputConfig,
NextConfig,
} from './types.js';
export { validBuildTargets, isValidBuildTarget } from './types.js';
export type { WorkflowManifest } from './apply-swc-transform.js';
export { applySwcTransform } from './apply-swc-transform.js';
Expand Down
48 changes: 43 additions & 5 deletions packages/builders/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@ export const validBuildTargets = [
] as const;
export type BuildTarget = (typeof validBuildTargets)[number];

export interface WorkflowConfig {
/**
* Common configuration options shared across all builder types.
*/
interface BaseWorkflowConfig {
watch?: boolean;
dirs: string[];
workingDir: string;
buildTarget: BuildTarget;
stepsBundlePath: string;
workflowsBundlePath: string;
webhookBundlePath: string;

// Optionally generate a client library for workflow execution. The preferred
// method of using workflow is to use a loader within a framework (like
Expand All @@ -24,6 +23,45 @@ export interface WorkflowConfig {
workflowManifestPath?: string;
}

/**
* Configuration for standalone (CLI-based) builds.
*/
export interface StandaloneConfig extends BaseWorkflowConfig {
buildTarget: 'standalone';
stepsBundlePath: string;
workflowsBundlePath: string;
webhookBundlePath: string;
}

/**
* Configuration for Vercel Build Output API builds.
*/
export interface VercelBuildOutputConfig extends BaseWorkflowConfig {
buildTarget: 'vercel-build-output-api';
stepsBundlePath: string;
workflowsBundlePath: string;
webhookBundlePath: string;
}

/**
* Configuration for Next.js builds.
*/
export interface NextConfig extends BaseWorkflowConfig {
buildTarget: 'next';
// Next.js builder computes paths dynamically, so these are not used
stepsBundlePath: string;
workflowsBundlePath: string;
webhookBundlePath: string;
}

/**
* Discriminated union of all builder configuration types.
*/
export type WorkflowConfig =
| StandaloneConfig
| VercelBuildOutputConfig
| NextConfig;

Copy link
Contributor

Choose a reason for hiding this comment

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

The isValidBuildTarget type guard function doesn't include the 'next' build target in its validation logic, but the new discriminated union WorkflowConfig now includes NextConfig with buildTarget: 'next'. This will cause type mismatches and incorrect validation.

View Details
📝 Patch Details
diff --git a/packages/builders/src/types.ts b/packages/builders/src/types.ts
index 8e2ae46..142d550 100644
--- a/packages/builders/src/types.ts
+++ b/packages/builders/src/types.ts
@@ -65,5 +65,5 @@ export type WorkflowConfig =
 export function isValidBuildTarget(
   target: string | undefined
 ): target is BuildTarget {
-  return target === 'standalone' || target === 'vercel-build-output-api';
+  return (validBuildTargets as readonly string[]).includes(target as string);
 }

Analysis

isValidBuildTarget() type guard doesn't validate 'next' build target

What fails: The isValidBuildTarget() type guard function in packages/builders/src/types.ts only validates 'standalone' and 'vercel-build-output-api' targets, but the BuildTarget type and validBuildTargets constant include 'next'. This causes the type guard to return false for a valid discriminant value that is part of the WorkflowConfig union type.

How to reproduce:

import { isValidBuildTarget, validBuildTargets, BuildTarget } from '@workflow/builders';

// validBuildTargets includes 'next'
console.log(validBuildTargets); // ['standalone', 'vercel-build-output-api', 'next']

// But the type guard rejects it
console.log(isValidBuildTarget('next')); // false (BUG)
console.log(isValidBuildTarget('standalone')); // true (works)
console.log(isValidBuildTarget('vercel-build-output-api')); // true (works)

Result: isValidBuildTarget('next') returns false, even though 'next' is:

  1. In the validBuildTargets constant
  2. A valid value for BuildTarget type
  3. Part of the WorkflowConfig discriminated union via NextConfig

This causes type safety violations - code constructing a NextConfig with buildTarget: 'next' would fail validation with the type guard.

Expected: The type guard should return true for all three valid build targets, matching the BuildTarget type definition derived from validBuildTargets.

Fix: Changed the implementation to use the validBuildTargets constant for validation rather than hardcoded literals. This ensures any new build targets added to validBuildTargets are automatically validated, preventing future regressions.

export function isValidBuildTarget(
target: string | undefined
): target is BuildTarget {
Expand Down
Loading