How does withWorkflow detect and resolve imports?
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?
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):
-
'use workflow'directive - matches file-level directive:export const useWorkflowPattern = /^\s*(['"])use workflow\1;?\s*$/m; -
'use step'directive - matches file-level directive:export const useStepPattern = /^\s*(['"])use step\1;?\s*$/m; -
@workflow/serdeimports - for custom class serialization:export const workflowSerdeImportPattern = /from\s+(['"])@workflow\/serde\1/; -
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.ts→index.jsin 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