Skip to content

Commit 577b5ee

Browse files
kdy1claude
andauthored
feat(flags): Add two-phase mode support (mark & shake) (#580)
## Summary Add support for two-phase feature flag transformation with `mark` and `shake` modes. ## Changes ### New Mode System - **Mark mode (default)**: Marks flags with `__SWC_FLAGS__` markers for later substitution - **Shake mode**: Substitutes markers with boolean values and performs DCE This provides a clearer two-phase workflow: 1. **Phase 1 (mark)**: Convert flag variables to markers 2. **Phase 2 (shake)**: Substitute markers and eliminate dead code ### Implementation - Add `TransformMode` enum with `Mark` and `Shake` variants - Add unified `FeatureFlagsConfig` supporting both modes - Update plugin to dispatch to `BuildTimeTransform` (mark) or `RuntimeTransform` (shake) - Mark mode is now the default (previously was implicit build-time behavior) ### TypeScript Types - Updated `types.d.ts` with new `TransformMode` type - Added comprehensive JSDoc documentation with examples - Maintained backward compatibility ### Tests - Added comprehensive WASM integration tests (`__tests__/wasm.test.ts`) - Tests for mark mode: marker generation, custom marker objects - Tests for shake mode: DCE on markers, logical operations - Tests for edge cases: exclude flags, nested scopes, multiple libraries - All 8 tests passing ✅ ### Documentation - Updated README with two-phase workflow explanation - Added complete examples showing both phases - Clarified when to use each mode ## Example Usage ### Mark Mode (Phase 1) ```json { "mode": "mark", "libraries": { "@your/flags": { "functions": ["useFlags"] } } } ``` ### Shake Mode (Phase 2) ```json { "mode": "shake", "flagValues": { "featureA": true, "featureB": false } } ``` ## Breaking Changes None - the plugin maintains backward compatibility with the old `BuildTimeConfig` format. ## Test Results - ✅ Rust crate tests: 5 passed - ✅ WASM integration tests: 8 passed - ✅ Build successful 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Sonnet 4.5 (1M context) <noreply@anthropic.com>
1 parent cdb3cca commit 577b5ee

10 files changed

Lines changed: 1076 additions & 52 deletions

File tree

contrib/mut-cjs-exports/src/local_export_strip.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,6 @@ impl VisitMut for LocalExportStrip {
214214
#[cfg(swc_ast_unknown)]
215215
_ => panic!("unknown node"),
216216
}))
217-
218217
}
219218

220219
/// ```javascript

crates/swc_feature_flags/src/config.rs

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,56 @@ use std::collections::HashMap;
22

33
use serde::Deserialize;
44

5+
/// Transform mode for feature flags
6+
#[derive(Clone, Debug, Deserialize, PartialEq)]
7+
#[serde(rename_all = "lowercase")]
8+
pub enum TransformMode {
9+
/// Mark mode: marks flags with __SWC_FLAGS__ markers for later substitution
10+
Mark,
11+
/// Shake mode: directly substitutes flag values and performs DCE (dead code
12+
/// elimination)
13+
Shake,
14+
}
15+
16+
impl Default for TransformMode {
17+
fn default() -> Self {
18+
Self::Mark
19+
}
20+
}
21+
22+
/// Unified configuration for feature flag transformation
23+
#[derive(Clone, Debug, Deserialize)]
24+
#[serde(rename_all = "camelCase")]
25+
pub struct FeatureFlagsConfig {
26+
/// Transform mode (default: "mark")
27+
#[serde(default)]
28+
pub mode: TransformMode,
29+
30+
/// Library configurations: library name -> config
31+
/// Required in mark mode, not used in shake mode
32+
#[serde(default)]
33+
pub libraries: HashMap<String, LibraryConfig>,
34+
35+
/// Flags to exclude from processing
36+
#[serde(default)]
37+
pub exclude_flags: Vec<String>,
38+
39+
/// Global object name for markers (default: "__SWC_FLAGS__")
40+
/// Only used in mark mode
41+
#[serde(default = "default_marker_object")]
42+
pub marker_object: String,
43+
44+
/// Flag values to apply (flag_name -> boolean)
45+
/// Required in shake mode
46+
#[serde(default)]
47+
pub flag_values: HashMap<String, bool>,
48+
49+
/// Whether to collect statistics (default: true)
50+
/// Only used in shake mode
51+
#[serde(default = "default_true")]
52+
pub collect_stats: bool,
53+
}
54+
555
/// Configuration for build-time feature flag transformation
656
#[derive(Clone, Debug, Deserialize)]
757
#[serde(rename_all = "camelCase")]
@@ -76,3 +126,16 @@ impl Default for RuntimeConfig {
76126
}
77127
}
78128
}
129+
130+
impl Default for FeatureFlagsConfig {
131+
fn default() -> Self {
132+
Self {
133+
mode: TransformMode::default(),
134+
libraries: HashMap::new(),
135+
exclude_flags: Vec::new(),
136+
marker_object: default_marker_object(),
137+
flag_values: HashMap::new(),
138+
collect_stats: true,
139+
}
140+
}
141+
}

crates/swc_feature_flags/src/lib.rs

Lines changed: 79 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,9 @@ pub mod stats;
5151

5252
// Re-exports for convenience
5353
pub use build_time::BuildTimeTransform;
54-
pub use config::{BuildTimeConfig, LibraryConfig, RuntimeConfig};
54+
pub use config::{
55+
BuildTimeConfig, FeatureFlagsConfig, LibraryConfig, RuntimeConfig, TransformMode,
56+
};
5557
pub use runtime::RuntimeTransform;
5658
pub use stats::TransformStats;
5759
use swc_ecma_ast::Pass;
@@ -111,3 +113,79 @@ pub fn build_time_pass(config: BuildTimeConfig) -> impl Pass {
111113
pub fn runtime_pass(config: RuntimeConfig) -> impl Pass {
112114
visit_mut_pass(RuntimeTransform::new(config))
113115
}
116+
117+
/// Create a unified feature flags pass based on the configured mode
118+
///
119+
/// This pass supports two modes:
120+
/// - **Mark mode** (default): Marks flags with `__SWC_FLAGS__` markers for
121+
/// later substitution. This is the first pass in a two-phase transformation.
122+
/// - **Shake mode**: Substitutes `__SWC_FLAGS__` markers with boolean values
123+
/// and performs DCE (dead code elimination). This is the second pass that
124+
/// processes output from mark mode.
125+
///
126+
/// # Two-Phase Workflow
127+
///
128+
/// 1. **Mark phase**: Use mark mode to convert flag variables to markers
129+
/// 2. **Shake phase**: Use shake mode to substitute markers and eliminate dead
130+
/// code
131+
///
132+
/// # Example
133+
///
134+
/// ```rust,ignore
135+
/// use std::collections::HashMap;
136+
/// use swc_feature_flags::{FeatureFlagsConfig, TransformMode, LibraryConfig};
137+
///
138+
/// // Phase 1: Mark mode (marker generation)
139+
/// let mark_config = FeatureFlagsConfig {
140+
/// mode: TransformMode::Mark,
141+
/// libraries: HashMap::from([
142+
/// ("@their/library".to_string(), LibraryConfig {
143+
/// functions: vec!["useExperimentalFlags".to_string()],
144+
/// }),
145+
/// ]),
146+
/// exclude_flags: vec![],
147+
/// marker_object: "__SWC_FLAGS__".to_string(),
148+
/// flag_values: HashMap::new(), // Not used in mark mode
149+
/// collect_stats: false,
150+
/// };
151+
///
152+
/// let program = program.apply(feature_flags_pass(mark_config));
153+
///
154+
/// // Phase 2: Shake mode (DCE)
155+
/// let shake_config = FeatureFlagsConfig {
156+
/// mode: TransformMode::Shake,
157+
/// libraries: HashMap::new(), // Not used in shake mode
158+
/// exclude_flags: vec![],
159+
/// marker_object: "__SWC_FLAGS__".to_string(),
160+
/// flag_values: HashMap::from([
161+
/// ("featureA".to_string(), true),
162+
/// ("featureB".to_string(), false),
163+
/// ]),
164+
/// collect_stats: true,
165+
/// };
166+
///
167+
/// let program = program.apply(feature_flags_pass(shake_config));
168+
/// ```
169+
pub fn feature_flags_pass(config: FeatureFlagsConfig) -> Box<dyn Pass> {
170+
match config.mode {
171+
TransformMode::Mark => {
172+
// Phase 1: Mark flags with __SWC_FLAGS__ markers
173+
let build_config = BuildTimeConfig {
174+
libraries: config.libraries,
175+
exclude_flags: config.exclude_flags,
176+
marker_object: config.marker_object,
177+
};
178+
Box::new(visit_mut_pass(BuildTimeTransform::new(build_config)))
179+
}
180+
TransformMode::Shake => {
181+
// Phase 2: Substitute markers and perform DCE
182+
let runtime_config = RuntimeConfig {
183+
flag_values: config.flag_values,
184+
remove_markers: true,
185+
collect_stats: config.collect_stats,
186+
marker_object: config.marker_object,
187+
};
188+
Box::new(visit_mut_pass(RuntimeTransform::new(runtime_config)))
189+
}
190+
}
191+
}

0 commit comments

Comments
 (0)