mirror of
https://github.com/vercel/workflow.git
synced 2026-09-14 19:59:43 +08:00
73bf7be925
## Summary
This PR changes how the SWC compiler generates IDs for workflows, steps, and classes. Instead of using raw file paths, IDs are now based on **Node.js module specifiers** when the file belongs to a package (either in `node_modules` or a workspace package).
## Motivation
Previously, IDs were generated using file paths like `step//src/jobs/order.ts//fetchData`. This caused several issues:
1. **Package exports conditions**: When a package uses conditional exports (e.g., `"workflow"` vs `"default"` conditions in `package.json`), the same import specifier can resolve to different files. Using file paths meant IDs could differ based on which export condition was used.
2. **Cross-bundle consistency**: Classes serialized in one bundle couldn't be deserialized in another if the file paths differed.
3. **Version tracking**: No way to include package versions in IDs for cache invalidation.
## Changes
### New ID Format
IDs now use the format `{type}//{modulePath}//{identifier}` where `modulePath` is either:
- A **module specifier** like `point@0.0.1` or `@myorg/shared@1.2.3` for package files
- A **relative path** prefixed with `./` like `./src/jobs/order` for local app files
Examples:
- `step//workflow@4.0.1-beta.50//fetch` (SDK step)
- `step//./workflows/order//processOrder` (local step)
- `class//point@0.0.1//Point` (package class)
- `class//./src/models/User//User` (local class)
### New Module Specifier Resolution
Added `packages/builders/src/module-specifier.ts` which:
- Detects if a file is in `node_modules` or a workspace package
- Finds the nearest `package.json` and extracts name/version
- Returns the module specifier for the SWC plugin to use
### SWC Plugin Changes
- Added `moduleSpecifier` option to plugin config
- Updated `naming.rs` to support both module specifiers and relative paths
- Added `get_module_path()` helper that uses specifier when available, falls back to `./filename` format
### Special Cases
- **Builtin functions** (`__builtin_*`): Continue to use just the function name as the ID for stable, version-independent lookup from the workflow VM runtime.
## Testing
- Updated all 125+ SWC plugin test fixtures to use new ID format
- Added tests for module specifier resolution
- Added tests for Windows path normalization in naming
## Breaking Changes
This is technically a breaking change for any persisted workflow runs that reference the old ID format. However, since IDs are internal implementation details and not user-facing, this should not affect end users.
## Files Changed
- `packages/builders/src/module-specifier.ts` - **NEW**: Module specifier resolution logic
- `packages/builders/src/apply-swc-transform.ts` - Pass module specifier to SWC plugin
- `packages/builders/src/base-builder.ts` - Use `getImportPath` for virtual entry imports
- `packages/swc-plugin-workflow/transform/src/lib.rs` - Accept and use module specifier
- `packages/swc-plugin-workflow/transform/src/naming.rs` - New ID formatting with module paths
- `packages/swc-plugin-workflow/spec.md` - Updated documentation
- `packages/core/e2e/e2e.test.ts` - Updated test assertions for new ID format
92 lines
3.3 KiB
Rust
92 lines
3.3 KiB
Rust
#![allow(clippy::not_unsafe_ptr_arg_deref)]
|
|
|
|
use serde::Deserialize;
|
|
use std::path::Path;
|
|
use swc_core::{
|
|
ecma::{ast::*, visit::*},
|
|
plugin::{plugin_transform, proxies::TransformPluginProgramMetadata},
|
|
};
|
|
use swc_workflow::{StepTransform, TransformMode};
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
|
struct WasmConfig {
|
|
mode: TransformMode,
|
|
/// The module specifier to use for ID generation.
|
|
///
|
|
/// This should be the canonical import specifier for this file, for example:
|
|
/// - "point@0.0.1" for a class from the `point` npm package
|
|
/// - "@myorg/shared@1.2.3" for a scoped package
|
|
///
|
|
/// If not provided, the plugin will use "./{relative_path}" format (e.g., "./src/models/Point").
|
|
///
|
|
/// This enables stable IDs across different export conditions in package.json,
|
|
/// where the same package specifier may resolve to different files depending on
|
|
/// the condition (e.g., "workflow" vs "default").
|
|
module_specifier: Option<String>,
|
|
}
|
|
|
|
#[plugin_transform]
|
|
pub fn process_transform(
|
|
mut program: Program,
|
|
metadata: TransformPluginProgramMetadata,
|
|
) -> Program {
|
|
let plugin_config: WasmConfig = serde_json::from_str(
|
|
&metadata
|
|
.get_transform_plugin_config()
|
|
.expect("failed to get plugin config for workflow transform"),
|
|
)
|
|
.expect("Should provide plugin config");
|
|
|
|
let filename = metadata
|
|
.get_context(&swc_core::plugin::metadata::TransformPluginMetadataContextKind::Filename)
|
|
.unwrap_or_else(|| "unknown".to_string());
|
|
|
|
// Try to get cwd and make the path relative
|
|
let cwd =
|
|
metadata.get_context(&swc_core::plugin::metadata::TransformPluginMetadataContextKind::Cwd);
|
|
|
|
let relative_filename = if let Some(cwd) = cwd {
|
|
let cwd_path = Path::new(&cwd);
|
|
let file_path = Path::new(&filename);
|
|
|
|
// Try to strip the cwd prefix to make it relative
|
|
if let Ok(relative) = file_path.strip_prefix(cwd_path) {
|
|
relative.to_string_lossy().to_string()
|
|
} else {
|
|
// Find common ancestor path
|
|
let cwd_components: Vec<_> = cwd_path.components().collect();
|
|
let file_components: Vec<_> = file_path.components().collect();
|
|
|
|
// Find the longest common prefix
|
|
let common_len = cwd_components
|
|
.iter()
|
|
.zip(file_components.iter())
|
|
.take_while(|(a, b)| a == b)
|
|
.count();
|
|
|
|
if common_len > 0 {
|
|
// Build relative path from the common ancestor
|
|
let remaining_file: Vec<_> = file_components.into_iter().skip(common_len).collect();
|
|
let relative_path = remaining_file.into_iter().collect::<std::path::PathBuf>();
|
|
relative_path.to_string_lossy().to_string()
|
|
} else {
|
|
filename
|
|
}
|
|
}
|
|
} else {
|
|
filename
|
|
};
|
|
|
|
// Normalize path separators to forward slashes for consistent workflow IDs across platforms
|
|
let normalized_filename = relative_filename.replace('\\', "/");
|
|
|
|
let mut visitor = StepTransform::new(
|
|
plugin_config.mode,
|
|
normalized_filename,
|
|
plugin_config.module_specifier,
|
|
);
|
|
program.visit_mut_with(&mut visitor);
|
|
program
|
|
}
|