Files
vercel__workflow/packages/builders/src/transform-utils.ts

169 lines
6.2 KiB
TypeScript

/**
* Shared utilities for detecting files that need workflow transformation.
* Used by builders, rollup plugin, Next.js loader, and vite plugin.
*/
// Matches: 'use workflow'; "use workflow"; 'use step'; "use step"; at line start with optional whitespace
export const useWorkflowPattern = /^\s*(['"])use workflow\1;?\s*$/m;
export const useStepPattern = /^\s*(['"])use step\1;?\s*$/m;
// Matches imports from '@workflow/serde' (for custom class serialization)
// e.g.: import { WORKFLOW_SERIALIZE } from '@workflow/serde';
export const workflowSerdeImportPattern = /from\s+(['"])@workflow\/serde\1/;
// Matches direct usage of Symbol.for('workflow-serialize') or Symbol.for('workflow-deserialize')
// e.g.: static [Symbol.for('workflow-serialize')](instance) { ... }
export const workflowSerdeSymbolPattern =
/Symbol\.for\s*\(\s*(['"])workflow-(?:serialize|deserialize)\1\s*\)/;
// Matches usage of WORKFLOW_SERIALIZE or WORKFLOW_DESERIALIZE as computed property names
// e.g.: static [WORKFLOW_SERIALIZE](instance) { ... }
// This catches cases where the symbols are imported and used (even if bundled/re-exported)
export const workflowSerdeComputedPropertyPattern =
/\[\s*WORKFLOW_(?:SERIALIZE|DESERIALIZE)\s*\]/;
const templateLiteralPattern = /`(?:\\[\s\S]|[^`\\])*`/g;
const commentPattern = /\/\*[\s\S]*?\*\/|\/\/[^\r\n]*/g;
const directiveLinePattern = /^\s*(['"])(use workflow|use step)\1;?\s*$/;
const stringDirectiveLinePattern = /^\s*(['"])[^'"]+\1;?\s*$/;
// Pattern to detect generated workflow route files that should be excluded
// These files are generated by the build process and should not be re-processed
export const generatedWorkflowPathPattern =
/[/\\]\.well-known[/\\]workflow[/\\]/;
function hasDirective(source: string, directive: 'use workflow' | 'use step') {
let previousMeaningfulLine: string | undefined;
for (const line of source.split(/\r?\n/)) {
const trimmedLine = line.trim();
if (trimmedLine === '') {
continue;
}
const directiveMatch = directiveLinePattern.exec(trimmedLine);
if (directiveMatch) {
if (
directiveMatch[2] === directive &&
(previousMeaningfulLine === undefined ||
previousMeaningfulLine.endsWith('{') ||
stringDirectiveLinePattern.test(previousMeaningfulLine))
) {
return true;
}
previousMeaningfulLine = trimmedLine;
continue;
}
previousMeaningfulLine = trimmedLine;
}
return false;
}
/**
* Detects workflow-related patterns in source code.
*/
export interface WorkflowPatternMatch {
/** File contains 'use workflow' directive */
hasUseWorkflow: boolean;
/** File contains 'use step' directive */
hasUseStep: boolean;
/** File contains @workflow/serde import */
hasSerdeImport: boolean;
/** File contains Symbol.for('workflow-serialize') or Symbol.for('workflow-deserialize') */
hasSerdeSymbol: boolean;
/** File contains any directive ('use workflow' or 'use step') */
hasDirective: boolean;
/** File contains any serde pattern (import or Symbol.for) */
hasSerde: boolean;
}
/**
* Detects workflow-related patterns in source code.
* @param source - The source code to analyze
* @returns Object with flags for each detected pattern
*/
export function detectWorkflowPatterns(source: string): WorkflowPatternMatch {
const hasDirectiveSubstring =
source.includes('use workflow') || source.includes('use step');
const sourceForDirectives =
hasDirectiveSubstring && (source.includes('`') || source.includes('/'))
? source
.replace(templateLiteralPattern, (match) =>
match.replace(/[^\r\n]/g, ' ')
)
.replace(commentPattern, (match) => match.replace(/[^\r\n]/g, ' '))
: source;
const hasUseWorkflow =
source.includes('use workflow') &&
hasDirective(sourceForDirectives, 'use workflow');
const hasUseStep =
source.includes('use step') &&
hasDirective(sourceForDirectives, 'use step');
const hasSerdeImport =
source.includes('@workflow/serde') &&
workflowSerdeImportPattern.test(source);
const hasSerdeSymbol =
(source.includes('workflow-serialize') ||
source.includes('workflow-deserialize')) &&
workflowSerdeSymbolPattern.test(source);
const hasSerdeComputedProperty =
(source.includes('WORKFLOW_SERIALIZE') ||
source.includes('WORKFLOW_DESERIALIZE')) &&
workflowSerdeComputedPropertyPattern.test(source);
return {
hasUseWorkflow,
hasUseStep,
hasSerdeImport,
hasSerdeSymbol,
hasDirective: hasUseWorkflow || hasUseStep,
hasSerde: hasSerdeImport || hasSerdeSymbol || hasSerdeComputedProperty,
};
}
/**
* Determines if a file path is a generated workflow route file.
* @param filePath - The file path to check
* @returns true if the file is a generated workflow route
*/
export function isGeneratedWorkflowFile(filePath: string): boolean {
return generatedWorkflowPathPattern.test(filePath);
}
/**
* Determines if a file should be transformed based on its path and content patterns.
*
* Logic:
* - Generated workflow route files are never transformed
* - Files with directives ('use workflow' or 'use step') are always transformed
* - Files with serde patterns are always transformed; the SWC plugin performs
* AST-level extraction/filtering to determine whether they actually define
* serde classes
*
* @param filePath - The file path to check
* @param patterns - The detected patterns from detectWorkflowPatterns()
* @returns true if the file should be transformed
*/
export function shouldTransformFile(
filePath: string,
patterns: WorkflowPatternMatch
): boolean {
// Never transform generated workflow route files
if (isGeneratedWorkflowFile(filePath)) {
return false;
}
return patterns.hasDirective || patterns.hasSerde;
}
/**
* Combined regex pattern for turbopack content matching.
* Uses backreferences to ensure matching quote types.
* Matches: 'use workflow', 'use step', @workflow/serde imports, Symbol.for serialization symbols,
* and WORKFLOW_SERIALIZE/WORKFLOW_DESERIALIZE computed property usage
*/
export const turbopackContentPattern =
/(use workflow|use step|from\s+(['"])@workflow\/serde\2|Symbol\.for\s*\(\s*(['"])workflow-(?:serialize|deserialize)\3\s*\)|\[\s*WORKFLOW_(?:SERIALIZE|DESERIALIZE)\s*\])/;