Skip to content
Draft
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/graph-step-traversal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@workflow/swc-plugin': patch
---

Fix graph mode traversal so it walks workflow bodies with a DFS pass, capturing direct calls, callbacks, and nested workflow references in the emitted graph manifest.

6 changes: 6 additions & 0 deletions .changeset/migrate-nuqs-url-state.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@workflow/web": patch
---

Migrate to nuqs for URL state management. Replaces custom URL parameter hooks with the nuqs library for better type-safety and simpler state management.

49 changes: 45 additions & 4 deletions packages/builders/src/apply-swc-transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,38 @@ export type WorkflowManifest = {
};
};

export type GraphManifest = {
version: string;
workflows: {
[workflowName: string]: {
workflowId: string;
workflowName: string;
filePath: string;
nodes: Array<{
id: string;
type: string;
position: { x: number; y: number };
data: {
label: string;
nodeKind: string;
stepId?: string;
line: number;
};
}>;
edges: Array<{
id: string;
source: string;
target: string;
type: string;
}>;
};
};
};

export async function applySwcTransform(
filename: string,
source: string,
mode: 'workflow' | 'step' | 'client' | false,
mode: 'workflow' | 'step' | 'client' | 'graph' | false,
jscConfig?: {
paths?: Record<string, string[]>;
// this must be absolute path
Expand All @@ -32,6 +60,7 @@ export async function applySwcTransform(
): Promise<{
code: string;
workflowManifest: WorkflowManifest;
graphManifest?: GraphManifest;
}> {
// Determine if this is a TypeScript file
const isTypeScript = filename.endsWith('.ts') || filename.endsWith('.tsx');
Expand Down Expand Up @@ -65,12 +94,24 @@ export async function applySwcTransform(
/\/\*\*__internal_workflows({.*?})\*\//s
);

const parsedWorkflows = JSON.parse(
workflowCommentMatch?.[1] || '{}'
) as WorkflowManifest;
const metadata = JSON.parse(workflowCommentMatch?.[1] || '{}');

const parsedWorkflows = {
steps: metadata.steps,
workflows: metadata.workflows,
} as WorkflowManifest;

// Extract graph manifest from separate comment
const graphCommentMatch = result.code.match(
/\/\*\*__workflow_graph({.*?})\*\//s
);
const graphManifest = graphCommentMatch?.[1]
? (JSON.parse(graphCommentMatch[1]) as GraphManifest)
: undefined;

return {
code: result.code,
workflowManifest: parsedWorkflows || {},
graphManifest,
};
}
89 changes: 88 additions & 1 deletion packages/builders/src/base-builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import enhancedResolveOriginal from 'enhanced-resolve';
import * as esbuild from 'esbuild';
import { findUp } from 'find-up';
import { glob } from 'tinyglobby';
import type { WorkflowManifest } from './apply-swc-transform.js';
import type { GraphManifest, WorkflowManifest } from './apply-swc-transform.js';
import { createDiscoverEntriesPlugin } from './discover-entries-esbuild-plugin.js';
import { createNodeModuleErrorPlugin } from './node-module-esbuild-plugin.js';
import { createSwcPlugin } from './swc-esbuild-plugin.js';
Expand Down Expand Up @@ -838,4 +838,91 @@ export const OPTIONS = handler;`;
// We're intentionally silently ignoring this error - creating .gitignore isn't critical
}
}

/**
* Creates a graph manifest JSON file by running the SWC plugin in 'graph' mode.
* The manifest contains React Flow-compatible graph data for visualizing workflows.
*/
protected async createGraphManifest({
inputFiles,
outfile,
tsBaseUrl,
tsPaths,
}: {
inputFiles: string[];
outfile: string;
tsBaseUrl?: string;
tsPaths?: Record<string, string[]>;
}): Promise<void> {
const graphBuildStart = Date.now();
console.log('Creating workflow graph manifest...');

const { discoveredWorkflows: workflowFiles } = await this.discoverEntries(
inputFiles,
dirname(outfile)
);

if (workflowFiles.length === 0) {
console.log('No workflow files found, skipping graph generation');
return;
}

// Import applySwcTransform dynamically
const { applySwcTransform } = await import('./apply-swc-transform.js');

// Aggregate all graph data from all workflow files
const combinedGraphManifest: GraphManifest = {
version: '1.0.0',
workflows: {},
};

for (const workflowFile of workflowFiles) {
try {
const source = await readFile(workflowFile, 'utf-8');
const normalizedWorkingDir = this.config.workingDir.replace(/\\/g, '/');
const normalizedFile = workflowFile.replace(/\\/g, '/');
let relativePath = relative(
normalizedWorkingDir,
normalizedFile
).replace(/\\/g, '/');
if (!relativePath.startsWith('.')) {
relativePath = `./${relativePath}`;
}

const { graphManifest } = await applySwcTransform(
relativePath,
source,
'graph',
{
paths: tsPaths,
baseUrl: tsBaseUrl,
}
);

if (graphManifest && graphManifest.workflows) {
// Merge the workflows from this file into the combined manifest
Object.assign(
combinedGraphManifest.workflows,
graphManifest.workflows
);
}
} catch (error) {
console.warn(
`Failed to extract graph from ${workflowFile}:`,
error instanceof Error ? error.message : String(error)
);
}
}

// Write the combined graph manifest
await this.ensureDirectory(outfile);
await writeFile(outfile, JSON.stringify(combinedGraphManifest, null, 2));

console.log(
`Created graph manifest with ${
Object.keys(combinedGraphManifest.workflows).length
} workflow(s)`,
`${Date.now() - graphBuildStart}ms`
);
}
}
21 changes: 21 additions & 0 deletions packages/builders/src/standalone.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export class StandaloneBuilder extends BaseBuilder {
await this.buildStepsBundle(options);
await this.buildWorkflowsBundle(options);
await this.buildWebhookFunction();
await this.buildGraphManifest(options);

await this.createClientLibrary();
}
Expand Down Expand Up @@ -76,4 +77,24 @@ export class StandaloneBuilder extends BaseBuilder {
outfile: webhookBundlePath,
});
}

private async buildGraphManifest({
inputFiles,
tsPaths,
tsBaseUrl,
}: {
inputFiles: string[];
tsBaseUrl?: string;
tsPaths?: Record<string, string[]>;
}): Promise<void> {
const graphManifestPath = this.resolvePath('.swc/graph-manifest.json');
await this.ensureDirectory(graphManifestPath);

await this.createGraphManifest({
inputFiles,
outfile: graphManifestPath,
tsBaseUrl,
tsPaths,
});
}
}
48 changes: 48 additions & 0 deletions packages/next/src/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ export async function getNextBuilder() {
const stepsBuildContext = await this.buildStepsFunction(options);
const workflowsBundle = await this.buildWorkflowsFunction(options);
await this.buildWebhookRoute({ workflowGeneratedDir });

// Write graph manifest to workflow data directory
const workflowDataDir = join(
this.config.workingDir,
'.next/workflow-data'
);
await mkdir(workflowDataDir, { recursive: true });
await this.createGraphManifest({
inputFiles: options.inputFiles,
outfile: join(workflowDataDir, 'graph-manifest.json'),
tsBaseUrl: options.tsBaseUrl,
tsPaths: options.tsPaths,
});

await this.writeFunctionsConfig(outputDir);

if (this.config.watch) {
Expand Down Expand Up @@ -166,6 +180,23 @@ export async function getNextBuilder() {
);
}
workflowsCtx = newWorkflowsCtx;

// Rebuild graph manifest to workflow data directory
try {
const workflowDataDir = join(
this.config.workingDir,
'.next/workflow-data'
);
await mkdir(workflowDataDir, { recursive: true });
await this.createGraphManifest({
inputFiles: options.inputFiles,
outfile: join(workflowDataDir, 'graph-manifest.json'),
tsBaseUrl: options.tsBaseUrl,
tsPaths: options.tsPaths,
});
} catch (error) {
console.error('Failed to rebuild graph manifest:', error);
}
};

const logBuildMessages = (
Expand Down Expand Up @@ -220,6 +251,23 @@ export async function getNextBuilder() {
'Rebuilt workflow bundle',
`${Date.now() - rebuiltWorkflowStart}ms`
);

// Rebuild graph manifest to workflow data directory
try {
const workflowDataDir = join(
this.config.workingDir,
'.next/workflow-data'
);
await mkdir(workflowDataDir, { recursive: true });
await this.createGraphManifest({
inputFiles: options.inputFiles,
outfile: join(workflowDataDir, 'graph-manifest.json'),
tsBaseUrl: options.tsBaseUrl,
tsPaths: options.tsPaths,
});
} catch (error) {
console.error('Failed to rebuild graph manifest:', error);
}
};

const isWatchableFile = (path: string) =>
Expand Down
Loading
Loading