How does withWorkflow detect and resolve imports?

HEAD
c1d7c8dAdd support for `"use step"` functions in class instance methods (#777)
This post might have stale content, as HEAD is 46 commits ahead.

How does the withWorkflow Next.js plugin handle detecting imports from the 'workflow' package? Does it hardcode the package name or does it support aliases? How does it resolve the loader path?

Avatar of anthropic/claude-opus-4.5
Claude Opus 4.5
commented

Now I have a complete picture. Let me provide a comprehensive answer:


##Summary: How withWorkflow Next.js Plugin Detects Workflow Imports

###1. File Detection is Directive-Based, Not Import-Based

The withWorkflow Next.js plugin does NOT detect imports from the 'workflow' package directly. Instead, it detects files that need transformation based on directives and serialization patterns:

####Detection Patterns (from packages/builders/src/transform-utils.ts):

  1. 'use workflow' directive - matches file-level directive:

    export const useWorkflowPattern = /^\s*(['"])use workflow\1;?\s*$/m;
    
  2. 'use step' directive - matches file-level directive:

    export const useStepPattern = /^\s*(['"])use step\1;?\s*$/m;
    
  3. @workflow/serde imports - for custom class serialization:

    export const workflowSerdeImportPattern = /from\s+(['"])@workflow\/serde\1/;
    
  4. Symbol.for('workflow-serialize/deserialize') patterns - direct symbol usage:

    export const workflowSerdeSymbolPattern =
      /Symbol\.for\s*\(\s*(['"])workflow-(?:serialize|deserialize)\1\s*\)/;
    

###2. Package Name Is Hardcoded - No Alias Support

The package name @workflow/serde is hardcoded in the regex pattern. There's no support for:

  • Package aliases (like @my-alias/serde)
  • Different package names
  • Path mappings from tsconfig.json

The SWC plugin (in Rust) also hardcodes the symbol names WORKFLOW_SERIALIZE and WORKFLOW_DESERIALIZE for tracking:

// packages/swc-plugin-workflow/transform/src/lib.rs
if imported_name == "WORKFLOW_SERIALIZE" {
    self.serialization_symbol_identifiers
        .insert(local_name, "workflow-serialize".to_string());
} else if imported_name == "WORKFLOW_DESERIALIZE" {
    // ...
}

###3. Loader Path Resolution

The loader path is resolved using Node.js's require.resolve():

// packages/next/src/index.ts, line 54
const loaderPath = require.resolve('./loader');

This resolves to the compiled loader.js file relative to the @workflow/next package. This works because:

  • require.resolve() uses Node's module resolution algorithm
  • './loader' is resolved relative to the current file (index.tsindex.js in the compiled output)
  • The resolved absolute path is then passed to both webpack and Turbopack

For webpack, it's used directly as a loader:

webpackConfig.module.rules.push({
  test: /.*\.(mjs|cjs|cts|ts|tsx|js|jsx)$/,
  loader: loaderPath,
});

For Turbopack, it's passed in the loaders array:

nextConfig.turbopack.rules[key] = {
  loaders: [...(existingRules[key]?.loaders || []), loaderPath],
};

###4. Turbopack Optimization (Next.js 16+)

For Turbopack in Next.js 16+, there's an optimization using a condition object to pre-filter files before running the loader:

condition: {
  all: [
    { not: { path: /[/\\]\.well-known[/\\]workflow[/\\]/ } },  // Exclude generated files
    { content: /(use workflow|use step|from\s+(['"])@workflow\/serde\2|...)/ },  // Match patterns
  ],
}

This allows Turbopack to skip files that don't contain workflow-related patterns, improving build performance.


END OF POST

How does withWorkflow detect and resolve imports? — vercel/workflow