-
Notifications
You must be signed in to change notification settings - Fork 129
[DRAFT] Workflow graph extraction and o11y updates #397
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from 11 commits
4a1788b
f073014
82240ef
ab87596
1dc7df5
acb109a
1bfd16d
4b4b55a
53dacc1
ef0ab99
0af4c2d
e00700f
bbf3b41
e274059
9a583fd
33237b2
f5cbe81
af497fd
080df95
9312eba
1aa86d8
5b6697a
0e0c522
4972ffe
de123cd
1e8def6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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. | ||
|
|
||
| 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. | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
|
@@ -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'); | ||
|
|
@@ -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, | ||
| }; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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'; | ||
|
|
@@ -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` | ||
| ); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
don't forget changeset for
builders,next, and all the other packages when you're ready to actually ssubmit the incremental PRs for review)