diff --git a/contrib/mut-cjs-exports/src/local_export_strip.rs b/contrib/mut-cjs-exports/src/local_export_strip.rs index 8d8e400d0..212354109 100644 --- a/contrib/mut-cjs-exports/src/local_export_strip.rs +++ b/contrib/mut-cjs-exports/src/local_export_strip.rs @@ -214,7 +214,6 @@ impl VisitMut for LocalExportStrip { #[cfg(swc_ast_unknown)] _ => panic!("unknown node"), })) - } /// ```javascript diff --git a/crates/swc_feature_flags/src/config.rs b/crates/swc_feature_flags/src/config.rs index 0c4f00acc..1aa9ab856 100644 --- a/crates/swc_feature_flags/src/config.rs +++ b/crates/swc_feature_flags/src/config.rs @@ -2,6 +2,56 @@ use std::collections::HashMap; use serde::Deserialize; +/// Transform mode for feature flags +#[derive(Clone, Debug, Deserialize, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum TransformMode { + /// Mark mode: marks flags with __SWC_FLAGS__ markers for later substitution + Mark, + /// Shake mode: directly substitutes flag values and performs DCE (dead code + /// elimination) + Shake, +} + +impl Default for TransformMode { + fn default() -> Self { + Self::Mark + } +} + +/// Unified configuration for feature flag transformation +#[derive(Clone, Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct FeatureFlagsConfig { + /// Transform mode (default: "mark") + #[serde(default)] + pub mode: TransformMode, + + /// Library configurations: library name -> config + /// Required in mark mode, not used in shake mode + #[serde(default)] + pub libraries: HashMap, + + /// Flags to exclude from processing + #[serde(default)] + pub exclude_flags: Vec, + + /// Global object name for markers (default: "__SWC_FLAGS__") + /// Only used in mark mode + #[serde(default = "default_marker_object")] + pub marker_object: String, + + /// Flag values to apply (flag_name -> boolean) + /// Required in shake mode + #[serde(default)] + pub flag_values: HashMap, + + /// Whether to collect statistics (default: true) + /// Only used in shake mode + #[serde(default = "default_true")] + pub collect_stats: bool, +} + /// Configuration for build-time feature flag transformation #[derive(Clone, Debug, Deserialize)] #[serde(rename_all = "camelCase")] @@ -76,3 +126,16 @@ impl Default for RuntimeConfig { } } } + +impl Default for FeatureFlagsConfig { + fn default() -> Self { + Self { + mode: TransformMode::default(), + libraries: HashMap::new(), + exclude_flags: Vec::new(), + marker_object: default_marker_object(), + flag_values: HashMap::new(), + collect_stats: true, + } + } +} diff --git a/crates/swc_feature_flags/src/lib.rs b/crates/swc_feature_flags/src/lib.rs index 84199def0..52c5d36c4 100644 --- a/crates/swc_feature_flags/src/lib.rs +++ b/crates/swc_feature_flags/src/lib.rs @@ -51,7 +51,9 @@ pub mod stats; // Re-exports for convenience pub use build_time::BuildTimeTransform; -pub use config::{BuildTimeConfig, LibraryConfig, RuntimeConfig}; +pub use config::{ + BuildTimeConfig, FeatureFlagsConfig, LibraryConfig, RuntimeConfig, TransformMode, +}; pub use runtime::RuntimeTransform; pub use stats::TransformStats; use swc_ecma_ast::Pass; @@ -111,3 +113,79 @@ pub fn build_time_pass(config: BuildTimeConfig) -> impl Pass { pub fn runtime_pass(config: RuntimeConfig) -> impl Pass { visit_mut_pass(RuntimeTransform::new(config)) } + +/// Create a unified feature flags pass based on the configured mode +/// +/// This pass supports two modes: +/// - **Mark mode** (default): Marks flags with `__SWC_FLAGS__` markers for +/// later substitution. This is the first pass in a two-phase transformation. +/// - **Shake mode**: Substitutes `__SWC_FLAGS__` markers with boolean values +/// and performs DCE (dead code elimination). This is the second pass that +/// processes output from mark mode. +/// +/// # Two-Phase Workflow +/// +/// 1. **Mark phase**: Use mark mode to convert flag variables to markers +/// 2. **Shake phase**: Use shake mode to substitute markers and eliminate dead +/// code +/// +/// # Example +/// +/// ```rust,ignore +/// use std::collections::HashMap; +/// use swc_feature_flags::{FeatureFlagsConfig, TransformMode, LibraryConfig}; +/// +/// // Phase 1: Mark mode (marker generation) +/// let mark_config = FeatureFlagsConfig { +/// mode: TransformMode::Mark, +/// libraries: HashMap::from([ +/// ("@their/library".to_string(), LibraryConfig { +/// functions: vec!["useExperimentalFlags".to_string()], +/// }), +/// ]), +/// exclude_flags: vec![], +/// marker_object: "__SWC_FLAGS__".to_string(), +/// flag_values: HashMap::new(), // Not used in mark mode +/// collect_stats: false, +/// }; +/// +/// let program = program.apply(feature_flags_pass(mark_config)); +/// +/// // Phase 2: Shake mode (DCE) +/// let shake_config = FeatureFlagsConfig { +/// mode: TransformMode::Shake, +/// libraries: HashMap::new(), // Not used in shake mode +/// exclude_flags: vec![], +/// marker_object: "__SWC_FLAGS__".to_string(), +/// flag_values: HashMap::from([ +/// ("featureA".to_string(), true), +/// ("featureB".to_string(), false), +/// ]), +/// collect_stats: true, +/// }; +/// +/// let program = program.apply(feature_flags_pass(shake_config)); +/// ``` +pub fn feature_flags_pass(config: FeatureFlagsConfig) -> Box { + match config.mode { + TransformMode::Mark => { + // Phase 1: Mark flags with __SWC_FLAGS__ markers + let build_config = BuildTimeConfig { + libraries: config.libraries, + exclude_flags: config.exclude_flags, + marker_object: config.marker_object, + }; + Box::new(visit_mut_pass(BuildTimeTransform::new(build_config))) + } + TransformMode::Shake => { + // Phase 2: Substitute markers and perform DCE + let runtime_config = RuntimeConfig { + flag_values: config.flag_values, + remove_markers: true, + collect_stats: config.collect_stats, + marker_object: config.marker_object, + }; + Box::new(visit_mut_pass(RuntimeTransform::new(runtime_config))) + } + } +} diff --git a/packages/feature-flags/README.md b/packages/feature-flags/README.md index 2511debd0..2e1a7d445 100644 --- a/packages/feature-flags/README.md +++ b/packages/feature-flags/README.md @@ -4,7 +4,11 @@ SWC plugin for build-time feature flag transformation. Part of the SWC Feature F ## Overview -This plugin performs build-time marking of feature flags by replacing flag identifiers with `__SWC_FLAGS__` markers. This enables aggressive dead code elimination in subsequent build steps. +This plugin provides two modes for feature flag transformation: + +- **Mark mode** (default): Marks flags with `__SWC_FLAGS__` markers for later substitution. Use this when you want to perform flag substitution in a separate build step. + +- **Shake mode**: Directly substitutes flag values with boolean literals and performs dead code elimination in a single pass. Use this when you know the flag values at build time. ## Installation @@ -14,7 +18,9 @@ npm install @swc/plugin-experimental-feature-flags ## Usage -Add to your `.swcrc`: +### Mark Mode (Default - Marker Generation) + +Use mark mode to mark flags for later substitution: ```json { @@ -22,6 +28,7 @@ Add to your `.swcrc`: "experimental": { "plugins": [ ["@swc/plugin-experimental-feature-flags", { + "mode": "mark", "libraries": { "@their/library": { "functions": ["useExperimentalFlags"] @@ -34,9 +41,38 @@ Add to your `.swcrc`: } ``` +### Shake Mode (Direct Optimization with DCE) + +Use shake mode to directly substitute flag values and eliminate dead code: + +```json +{ + "jsc": { + "experimental": { + "plugins": [ + ["@swc/plugin-experimental-feature-flags", { + "mode": "shake", + "libraries": { + "@their/library": { + "functions": ["useExperimentalFlags"] + } + }, + "flagValues": { + "featureA": true, + "featureB": false + } + }] + ] + } + } +} +``` + ## How It Works -**Before:** +### Mark Mode Example + +**Input:** ```javascript import { useExperimentalFlags } from '@their/library'; @@ -51,7 +87,7 @@ function App() { } ``` -**After:** +**Output:** ```javascript function App() { if (__SWC_FLAGS__.featureA) { @@ -62,16 +98,47 @@ function App() { } ``` -The plugin: +The plugin in mark mode: 1. Removes import statements from configured libraries 2. Detects destructuring patterns from configured functions 3. Replaces all flag identifier references with `__SWC_FLAGS__.flagName` 4. Removes the hook call statements +### Shake Mode Example + +**Input:** (same as above) + +**Output (with `featureA: true, featureB: false`):** +```javascript +function App() { + console.log('Feature A enabled'); + return 'Stable'; +} +``` + +The plugin in shake mode: +1. Removes import statements from configured libraries +2. Detects destructuring patterns from configured functions +3. Directly substitutes flag identifiers with boolean literals +4. Performs dead code elimination (DCE) +5. Removes the hook call statements + ## Configuration ```typescript -interface BuildTimeConfig { +interface FeatureFlagsConfig { + /** + * Transformation mode + * + * - "mark" (default): Marker-based - replaces flags with __SWC_FLAGS__.flagName + * for later substitution + * - "shake": Direct optimization - substitutes flags with boolean values + * and performs DCE immediately + * + * @default "mark" + */ + mode?: "mark" | "shake"; + /** * Library configurations: library name -> config * @@ -88,10 +155,7 @@ interface BuildTimeConfig { libraries: Record; /** - * Flags to exclude from build-time marking - * - * These flags will not be transformed and will remain as-is. - * Useful for flags that don't need dead code elimination. + * Flags to exclude from transformation * * @default [] */ @@ -99,10 +163,31 @@ interface BuildTimeConfig { /** * Global object name for markers + * Only used in mark mode * * @default "__SWC_FLAGS__" */ markerObject?: string; + + /** + * Flag values to apply (flag_name -> boolean) + * Required in shake mode, not used in mark mode + * + * @example + * { + * "featureA": true, + * "featureB": false + * } + */ + flagValues?: Record; + + /** + * Whether to collect transformation statistics + * Only used in shake mode + * + * @default true + */ + collectStats?: boolean; } interface LibraryConfig { @@ -188,6 +273,66 @@ You can customize the marker object name: } ``` +## Two-Phase Workflow + +This plugin uses a **two-phase approach** for feature flag transformation: + +1. **Phase 1 (Mark mode)**: Convert flag variables to `__SWC_FLAGS__` markers +2. **Phase 2 (Shake mode)**: Substitute markers with boolean values and eliminate dead code + +### When to Use Each Mode + +**Mark Mode (Phase 1)** +- Use in your initial build/compilation step +- Converts flag variables from your flag library into markers +- Output code still contains conditional logic (no DCE yet) +- Run this on your source code before bundling + +**Shake Mode (Phase 2)** +- Use after mark mode, when you know flag values +- Substitutes `__SWC_FLAGS__` markers with actual boolean values +- Performs dead code elimination (DCE) +- Run this in your build pipeline with environment-specific flag values + +### Example Workflow + +```bash +# Phase 1: Mark flags in source code +# swc.config.json +{ + "jsc": { + "experimental": { + "plugins": [ + ["@swc/plugin-experimental-feature-flags", { + "mode": "mark", + "libraries": { + "@your/flags": { "functions": ["useFlags"] } + } + }] + ] + } + } +} + +# Phase 2: Shake (DCE) with environment-specific values +# swc.prod.config.json +{ + "jsc": { + "experimental": { + "plugins": [ + ["@swc/plugin-experimental-feature-flags", { + "mode": "shake", + "flagValues": { + "featureA": true, + "featureB": false + } + }] + ] + } + } +} +``` + ## Scope Safety The plugin correctly handles variable shadowing using SWC's syntax context system: @@ -198,7 +343,7 @@ import { useExperimentalFlags } from '@their/library'; function App() { const { featureA } = useExperimentalFlags(); - if (featureA) { // Replaced with __SWC_FLAGS__.featureA + if (featureA) { // Replaced (mode dependent) const featureA = false; // Shadowed variable if (featureA) { // NOT replaced - uses local variable console.log('This uses the local featureA'); @@ -207,14 +352,44 @@ function App() { } ``` -## Next Steps: Runtime Dead Code Elimination +## Complete Two-Phase Example + +Here's a complete workflow showing both phases: + +**Step 1: Source Code** +```javascript +import { useFlags } from '@your/flags'; + +function App() { + const { newUI, betaFeature } = useFlags(); + + if (newUI) { + return ; + } + + return betaFeature ? : ; +} +``` + +**Step 2: After Mark Mode** +```javascript +function App() { + if (__SWC_FLAGS__.newUI) { + return ; + } -After build-time transformation, use the `swc_feature_flags` Rust crate to: -1. Substitute `__SWC_FLAGS__.flagName` with actual boolean values -2. Eliminate dead code branches (if statements, ternary, logical operators) -3. Track statistics (bytes removed, branches eliminated) + return __SWC_FLAGS__.betaFeature ? : ; +} +``` + +**Step 3: After Shake Mode (with `newUI: true, betaFeature: false`)** +```javascript +function App() { + return ; +} +``` -See the [`swc_feature_flags` crate documentation](../../crates/swc_feature_flags/README.md) for more details. +All dead code has been eliminated! ## TypeScript Support diff --git a/packages/feature-flags/README.tmpl.md b/packages/feature-flags/README.tmpl.md index 666f9a0e2..4e5b8cc92 100644 --- a/packages/feature-flags/README.tmpl.md +++ b/packages/feature-flags/README.tmpl.md @@ -4,7 +4,11 @@ SWC plugin for build-time feature flag transformation. Part of the SWC Feature F ## Overview -This plugin performs build-time marking of feature flags by replacing flag identifiers with `__SWC_FLAGS__` markers. This enables aggressive dead code elimination in subsequent build steps. +This plugin provides two modes for feature flag transformation: + +- **Mark mode** (default): Marks flags with `__SWC_FLAGS__` markers for later substitution. Use this when you want to perform flag substitution in a separate build step. + +- **Shake mode**: Directly substitutes flag values with boolean literals and performs dead code elimination in a single pass. Use this when you know the flag values at build time. ## Installation @@ -14,7 +18,9 @@ npm install @swc/plugin-experimental-feature-flags ## Usage -Add to your `.swcrc`: +### Mark Mode (Default - Marker Generation) + +Use mark mode to mark flags for later substitution: ```json { @@ -22,6 +28,7 @@ Add to your `.swcrc`: "experimental": { "plugins": [ ["@swc/plugin-experimental-feature-flags", { + "mode": "mark", "libraries": { "@their/library": { "functions": ["useExperimentalFlags"] @@ -34,9 +41,38 @@ Add to your `.swcrc`: } ``` +### Shake Mode (Direct Optimization with DCE) + +Use shake mode to directly substitute flag values and eliminate dead code: + +```json +{ + "jsc": { + "experimental": { + "plugins": [ + ["@swc/plugin-experimental-feature-flags", { + "mode": "shake", + "libraries": { + "@their/library": { + "functions": ["useExperimentalFlags"] + } + }, + "flagValues": { + "featureA": true, + "featureB": false + } + }] + ] + } + } +} +``` + ## How It Works -**Before:** +### Mark Mode Example + +**Input:** ```javascript import { useExperimentalFlags } from '@their/library'; @@ -51,7 +87,7 @@ function App() { } ``` -**After:** +**Output:** ```javascript function App() { if (__SWC_FLAGS__.featureA) { @@ -62,16 +98,47 @@ function App() { } ``` -The plugin: +The plugin in mark mode: 1. Removes import statements from configured libraries 2. Detects destructuring patterns from configured functions 3. Replaces all flag identifier references with `__SWC_FLAGS__.flagName` 4. Removes the hook call statements +### Shake Mode Example + +**Input:** (same as above) + +**Output (with `featureA: true, featureB: false`):** +```javascript +function App() { + console.log('Feature A enabled'); + return 'Stable'; +} +``` + +The plugin in shake mode: +1. Removes import statements from configured libraries +2. Detects destructuring patterns from configured functions +3. Directly substitutes flag identifiers with boolean literals +4. Performs dead code elimination (DCE) +5. Removes the hook call statements + ## Configuration ```typescript -interface BuildTimeConfig { +interface FeatureFlagsConfig { + /** + * Transformation mode + * + * - "mark" (default): Marker-based - replaces flags with __SWC_FLAGS__.flagName + * for later substitution + * - "shake": Direct optimization - substitutes flags with boolean values + * and performs DCE immediately + * + * @default "mark" + */ + mode?: "mark" | "shake"; + /** * Library configurations: library name -> config * @@ -88,10 +155,7 @@ interface BuildTimeConfig { libraries: Record; /** - * Flags to exclude from build-time marking - * - * These flags will not be transformed and will remain as-is. - * Useful for flags that don't need dead code elimination. + * Flags to exclude from transformation * * @default [] */ @@ -99,10 +163,31 @@ interface BuildTimeConfig { /** * Global object name for markers + * Only used in mark mode * * @default "__SWC_FLAGS__" */ markerObject?: string; + + /** + * Flag values to apply (flag_name -> boolean) + * Required in shake mode, not used in mark mode + * + * @example + * { + * "featureA": true, + * "featureB": false + * } + */ + flagValues?: Record; + + /** + * Whether to collect transformation statistics + * Only used in shake mode + * + * @default true + */ + collectStats?: boolean; } interface LibraryConfig { @@ -188,6 +273,66 @@ You can customize the marker object name: } ``` +## Two-Phase Workflow + +This plugin uses a **two-phase approach** for feature flag transformation: + +1. **Phase 1 (Mark mode)**: Convert flag variables to `__SWC_FLAGS__` markers +2. **Phase 2 (Shake mode)**: Substitute markers with boolean values and eliminate dead code + +### When to Use Each Mode + +**Mark Mode (Phase 1)** +- Use in your initial build/compilation step +- Converts flag variables from your flag library into markers +- Output code still contains conditional logic (no DCE yet) +- Run this on your source code before bundling + +**Shake Mode (Phase 2)** +- Use after mark mode, when you know flag values +- Substitutes `__SWC_FLAGS__` markers with actual boolean values +- Performs dead code elimination (DCE) +- Run this in your build pipeline with environment-specific flag values + +### Example Workflow + +```bash +# Phase 1: Mark flags in source code +# swc.config.json +{ + "jsc": { + "experimental": { + "plugins": [ + ["@swc/plugin-experimental-feature-flags", { + "mode": "mark", + "libraries": { + "@your/flags": { "functions": ["useFlags"] } + } + }] + ] + } + } +} + +# Phase 2: Shake (DCE) with environment-specific values +# swc.prod.config.json +{ + "jsc": { + "experimental": { + "plugins": [ + ["@swc/plugin-experimental-feature-flags", { + "mode": "shake", + "flagValues": { + "featureA": true, + "featureB": false + } + }] + ] + } + } +} +``` + ## Scope Safety The plugin correctly handles variable shadowing using SWC's syntax context system: @@ -198,7 +343,7 @@ import { useExperimentalFlags } from '@their/library'; function App() { const { featureA } = useExperimentalFlags(); - if (featureA) { // Replaced with __SWC_FLAGS__.featureA + if (featureA) { // Replaced (mode dependent) const featureA = false; // Shadowed variable if (featureA) { // NOT replaced - uses local variable console.log('This uses the local featureA'); @@ -207,20 +352,50 @@ function App() { } ``` -## Next Steps: Runtime Dead Code Elimination +## Complete Two-Phase Example + +Here's a complete workflow showing both phases: + +**Step 1: Source Code** +```javascript +import { useFlags } from '@your/flags'; + +function App() { + const { newUI, betaFeature } = useFlags(); + + if (newUI) { + return ; + } + + return betaFeature ? : ; +} +``` + +**Step 2: After Mark Mode** +```javascript +function App() { + if (__SWC_FLAGS__.newUI) { + return ; + } -After build-time transformation, use the `swc_feature_flags` Rust crate to: -1. Substitute `__SWC_FLAGS__.flagName` with actual boolean values -2. Eliminate dead code branches (if statements, ternary, logical operators) -3. Track statistics (bytes removed, branches eliminated) + return __SWC_FLAGS__.betaFeature ? : ; +} +``` + +**Step 3: After Shake Mode (with `newUI: true, betaFeature: false`)** +```javascript +function App() { + return ; +} +``` -See the [`swc_feature_flags` crate documentation](../../crates/swc_feature_flags/README.md) for more details. +All dead code has been eliminated! ## TypeScript Support This package includes TypeScript definitions. See `types.d.ts` for the full API. -# CHANGELOG +# ChangeLog $CHANGELOG diff --git a/packages/feature-flags/__tests__/__snapshots__/wasm.test.ts.snap b/packages/feature-flags/__tests__/__snapshots__/wasm.test.ts.snap new file mode 100644 index 000000000..698029bfb --- /dev/null +++ b/packages/feature-flags/__tests__/__snapshots__/wasm.test.ts.snap @@ -0,0 +1,75 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`Configuration defaults > Should default to mark mode when mode is not specified 1`] = ` +"function App() { + return __SWC_FLAGS__.featureA ? 'On' : 'Off'; +} +" +`; + +exports[`Edge cases > Should handle exclude flags in mark mode 1`] = ` +"function App() { + if (__SWC_FLAGS__.includedFlag) { + console.log('Included'); + } + if (excludedFlag) { + console.log('Excluded'); + } +} +" +`; + +exports[`Edge cases > Should handle nested scopes correctly in mark mode 1`] = ` +"function App() { + if (__SWC_FLAGS__.featureA) { + const featureA = false; // Shadowed variable + if (featureA) { + console.log('This uses the local featureA'); + } + } +} +" +`; + +exports[`Mark mode (default) - marker generation > Should mark flags with __SWC_FLAGS__ markers 1`] = ` +"function App() { + if (__SWC_FLAGS__.featureA) { + console.log('Feature A is enabled'); + } + return __SWC_FLAGS__.featureB ? 'Beta' : 'Stable'; +} +" +`; + +exports[`Mark mode (default) - marker generation > Should support custom marker object 1`] = ` +"function App() { + return __CUSTOM_FLAGS__.featureA ? 'On' : 'Off'; +} +" +`; + +exports[`Multiple libraries > Should handle multiple library sources in mark mode 1`] = ` +"function App() { + return __SWC_FLAGS__.featureA && __SWC_FLAGS__.featureB ? 'Both' : 'Neither'; +} +" +`; + +exports[`Shake mode - DCE on markers > Should handle complex logical operations 1`] = ` +"function App() { + // Logical AND with true + const useNew = hasPermission(); + // Logical OR with false + const useLegacy = !isModern(); + return 'feature-only'; +} +" +`; + +exports[`Shake mode - DCE on markers > Should substitute markers and eliminate dead code 1`] = ` +"function App() { + console.log('Feature A is enabled'); + return 'Stable'; +} +" +`; diff --git a/packages/feature-flags/__tests__/wasm.test.ts b/packages/feature-flags/__tests__/wasm.test.ts new file mode 100644 index 000000000..5c2971490 --- /dev/null +++ b/packages/feature-flags/__tests__/wasm.test.ts @@ -0,0 +1,272 @@ +import { expect, test, describe } from "vitest"; +import { transform } from "@swc/core"; +import path from "node:path"; +import url from "node:url"; + +const pluginName = "swc_plugin_experimental_feature_flags.wasm"; + +const transformCode = async (code: string, options = {}) => { + return transform(code, { + jsc: { + parser: { + syntax: "ecmascript", + jsx: true, + }, + target: "es2018", + experimental: { + plugins: [ + [ + path.join( + path.dirname(url.fileURLToPath(import.meta.url)), + "..", + pluginName, + ), + options, + ], + ], + }, + }, + filename: "test.js", + }); +}; + +describe("Mark mode (default) - marker generation", () => { + test("Should mark flags with __SWC_FLAGS__ markers", async () => { + const input = `import { useExperimentalFlags } from '@their/library'; + +function App() { + const { featureA, featureB } = useExperimentalFlags(); + + if (featureA) { + console.log('Feature A is enabled'); + } + + return featureB ? 'Beta' : 'Stable'; +}`; + + const { code } = await transformCode(input, { + mode: "mark", + libraries: { + "@their/library": { + functions: ["useExperimentalFlags"], + }, + }, + }); + + expect(code).toMatchSnapshot(); + // Should have eliminated the import + expect(code).not.toContain("import"); + expect(code).not.toContain("useExperimentalFlags"); + // Should have markers + expect(code).toContain("__SWC_FLAGS__.featureA"); + expect(code).toContain("__SWC_FLAGS__.featureB"); + // Should NOT have performed DCE + expect(code).toContain("? 'Beta' : 'Stable'"); + }); + + test("Should support custom marker object", async () => { + const input = `import { useExperimentalFlags } from '@their/library'; + +function App() { + const { featureA } = useExperimentalFlags(); + return featureA ? 'On' : 'Off'; +}`; + + const { code } = await transformCode(input, { + mode: "mark", + libraries: { + "@their/library": { + functions: ["useExperimentalFlags"], + }, + }, + markerObject: "__CUSTOM_FLAGS__", + }); + + expect(code).toMatchSnapshot(); + expect(code).toContain("__CUSTOM_FLAGS__.featureA"); + expect(code).not.toContain("__SWC_FLAGS__"); + }); +}); + +describe("Shake mode - DCE on markers", () => { + test("Should substitute markers and eliminate dead code", async () => { + const input = `function App() { + if (__SWC_FLAGS__.featureA) { + console.log('Feature A is enabled'); + } + + return __SWC_FLAGS__.featureB ? 'Beta' : 'Stable'; +}`; + + const { code } = await transformCode(input, { + mode: "shake", + flagValues: { + featureA: true, + featureB: false, + }, + }); + + expect(code).toMatchSnapshot(); + // Should have eliminated markers + expect(code).not.toContain("__SWC_FLAGS__"); + // Should have optimized the if statement + expect(code).toContain("console.log('Feature A is enabled')"); + // Should have optimized the ternary + expect(code).toContain("'Stable'"); + expect(code).not.toContain("'Beta'"); + }); + + test("Should handle complex logical operations", async () => { + const input = `function App() { + // Logical AND with true + const useNew = __SWC_FLAGS__.enableFeature && hasPermission(); + + // Logical OR with false + const useLegacy = __SWC_FLAGS__.showBeta || !isModern(); + + if (__SWC_FLAGS__.enableFeature && __SWC_FLAGS__.showBeta) { + return 'both'; + } else if (__SWC_FLAGS__.enableFeature) { + return 'feature-only'; + } else { + return 'none'; + } +}`; + + const { code } = await transformCode(input, { + mode: "shake", + flagValues: { + enableFeature: true, + showBeta: false, + }, + }); + + expect(code).toMatchSnapshot(); + expect(code).not.toContain("__SWC_FLAGS__"); + expect(code).toContain("hasPermission()"); + expect(code).toContain("!isModern()"); + expect(code).toContain("'feature-only'"); + }); +}); + +describe("Configuration defaults", () => { + test("Should default to mark mode when mode is not specified", async () => { + const input = `import { useExperimentalFlags } from '@their/library'; + +function App() { + const { featureA } = useExperimentalFlags(); + return featureA ? 'On' : 'Off'; +}`; + + // When mode is not specified, it defaults to "mark" + const { code } = await transformCode(input, { + libraries: { + "@their/library": { + functions: ["useExperimentalFlags"], + }, + }, + }); + + expect(code).toMatchSnapshot(); + // Should have created markers (mark mode is default) + expect(code).toContain("__SWC_FLAGS__.featureA"); + }); +}); + +describe("Multiple libraries", () => { + test("Should handle multiple library sources in mark mode", async () => { + const input = `import { useExperimentalFlags } from '@their/library'; +import { getFlags } from '@another/flags'; + +function App() { + const { featureA } = useExperimentalFlags(); + const { featureB } = getFlags(); + + return featureA && featureB ? 'Both' : 'Neither'; +}`; + + const { code } = await transformCode(input, { + mode: "mark", + libraries: { + "@their/library": { + functions: ["useExperimentalFlags"], + }, + "@another/flags": { + functions: ["getFlags"], + }, + }, + }); + + expect(code).toMatchSnapshot(); + expect(code).not.toContain("import"); + expect(code).not.toContain("useExperimentalFlags"); + expect(code).not.toContain("getFlags"); + expect(code).toContain("__SWC_FLAGS__.featureA"); + expect(code).toContain("__SWC_FLAGS__.featureB"); + }); +}); + +describe("Edge cases", () => { + test("Should handle exclude flags in mark mode", async () => { + const input = `import { useExperimentalFlags } from '@their/library'; + +function App() { + const { includedFlag, excludedFlag } = useExperimentalFlags(); + + if (includedFlag) { + console.log('Included'); + } + + if (excludedFlag) { + console.log('Excluded'); + } +}`; + + const { code } = await transformCode(input, { + mode: "mark", + libraries: { + "@their/library": { + functions: ["useExperimentalFlags"], + }, + }, + excludeFlags: ["excludedFlag"], + }); + + expect(code).toMatchSnapshot(); + // includedFlag should be marked + expect(code).toContain("__SWC_FLAGS__.includedFlag"); + // excludedFlag should remain as variable + expect(code).toContain("excludedFlag"); + expect(code).not.toContain("__SWC_FLAGS__.excludedFlag"); + }); + + test("Should handle nested scopes correctly in mark mode", async () => { + const input = `import { useExperimentalFlags } from '@their/library'; + +function App() { + const { featureA } = useExperimentalFlags(); + + if (featureA) { + const featureA = false; // Shadowed variable + if (featureA) { + console.log('This uses the local featureA'); + } + } +}`; + + const { code } = await transformCode(input, { + mode: "mark", + libraries: { + "@their/library": { + functions: ["useExperimentalFlags"], + }, + }, + }); + + expect(code).toMatchSnapshot(); + // Outer featureA should be marked + expect(code).toContain("__SWC_FLAGS__.featureA"); + // Inner shadowed variable should remain + expect(code).toContain("false"); + }); +}); diff --git a/packages/feature-flags/package.json b/packages/feature-flags/package.json index 93dca0376..83dddccdf 100644 --- a/packages/feature-flags/package.json +++ b/packages/feature-flags/package.json @@ -11,7 +11,8 @@ "scripts": { "prepack": "pnpm run build", "build": "RUSTFLAGS='--cfg swc_ast_unknown' cargo build --release -p swc_plugin_experimental_feature_flags --target wasm32-wasip1 && cp ../../target/wasm32-wasip1/release/swc_plugin_experimental_feature_flags.wasm .", - "build:debug": "RUSTFLAGS='--cfg swc_ast_unknown' cargo build -p swc_plugin_experimental_feature_flags --target wasm32-wasip1 && cp ../../target/wasm32-wasip1/debug/swc_plugin_experimental_feature_flags.wasm ." + "build:debug": "RUSTFLAGS='--cfg swc_ast_unknown' cargo build -p swc_plugin_experimental_feature_flags --target wasm32-wasip1 && cp ../../target/wasm32-wasip1/debug/swc_plugin_experimental_feature_flags.wasm .", + "test": "pnpm run build:debug && vitest run" }, "homepage": "https://swc.rs", "repository": { diff --git a/packages/feature-flags/src/lib.rs b/packages/feature-flags/src/lib.rs index 8e1623dd0..bdfd85f08 100644 --- a/packages/feature-flags/src/lib.rs +++ b/packages/feature-flags/src/lib.rs @@ -4,26 +4,61 @@ use swc_core::{ ecma::{ast::Program, visit::VisitMutWith}, plugin::{plugin_transform, proxies::TransformPluginProgramMetadata}, }; -use swc_feature_flags::{BuildTimeConfig, BuildTimeTransform}; +use swc_feature_flags::{ + BuildTimeConfig, BuildTimeTransform, FeatureFlagsConfig, RuntimeConfig, RuntimeTransform, + TransformMode, +}; /// SWC plugin entry point for feature flag transformation /// -/// This plugin performs build-time marking of feature flags by: -/// - Tracking imports from configured libraries -/// - Detecting destructuring from configured flag functions -/// - Replacing flag identifiers with `__SWC_FLAGS__.flagName` -/// - Removing import statements and hook calls +/// This plugin supports two modes: +/// - **Mark mode** (default): Marks flags with `__SWC_FLAGS__` markers for +/// later substitution. This is phase 1 of a two-phase transformation. +/// - **Shake mode**: Substitutes `__SWC_FLAGS__` markers with boolean values +/// and performs DCE (dead code elimination). This is phase 2. +/// +/// The plugin will first try to parse as FeatureFlagsConfig (new unified +/// config), and fall back to BuildTimeConfig (legacy config) for backward +/// compatibility. #[plugin_transform] fn swc_plugin_feature_flags(mut program: Program, data: TransformPluginProgramMetadata) -> Program { - let config = serde_json::from_str::( - &data - .get_transform_plugin_config() - .expect("failed to get plugin config"), - ) - .expect("invalid config"); + let config_str = &data + .get_transform_plugin_config() + .expect("failed to get plugin config"); + + // Try to parse as new unified config first + if let Ok(config) = serde_json::from_str::(config_str) { + match config.mode { + TransformMode::Mark => { + // Phase 1: Mark flags with __SWC_FLAGS__ markers + let build_config = BuildTimeConfig { + libraries: config.libraries, + exclude_flags: config.exclude_flags, + marker_object: config.marker_object, + }; + let mut transform = BuildTimeTransform::new(build_config); + program.visit_mut_with(&mut transform); + } + TransformMode::Shake => { + // Phase 2: Substitute markers and perform DCE + let runtime_config = RuntimeConfig { + flag_values: config.flag_values, + remove_markers: true, + collect_stats: config.collect_stats, + marker_object: config.marker_object, + }; + let mut transform = RuntimeTransform::new(runtime_config); + program.visit_mut_with(&mut transform); + } + } + } else { + // Fall back to old BuildTimeConfig for backward compatibility + let config = serde_json::from_str::(config_str) + .expect("invalid config: must be either FeatureFlagsConfig or BuildTimeConfig"); - let mut transform = BuildTimeTransform::new(config); - program.visit_mut_with(&mut transform); + let mut transform = BuildTimeTransform::new(config); + program.visit_mut_with(&mut transform); + } program } diff --git a/packages/feature-flags/types.d.ts b/packages/feature-flags/types.d.ts index 6d30745a7..98be06974 100644 --- a/packages/feature-flags/types.d.ts +++ b/packages/feature-flags/types.d.ts @@ -5,6 +5,11 @@ */ declare module "@swc/plugin-experimental-feature-flags" { + /** + * Transform mode for feature flags + */ + export type TransformMode = "mark" | "shake"; + /** * Configuration for a single library */ @@ -16,8 +21,128 @@ declare module "@swc/plugin-experimental-feature-flags" { functions: string[]; } + /** + * Unified configuration for the feature flags plugin + * + * Supports two modes: + * - **mark** (default): Marks flags with `__SWC_FLAGS__` markers for later + * substitution. Use this when you want to perform flag substitution + * in a separate build step. + * - **shake**: Directly substitutes flag values with boolean literals and + * performs dead code elimination in a single pass. Use this for direct + * optimization when you know flag values at build time. + * + * @example Mark mode (marker generation) + * ```json + * { + * "mode": "mark", + * "libraries": { + * "@their/library": { + * "functions": ["useExperimentalFlags"] + * } + * } + * } + * ``` + * + * @example Shake mode (direct optimization with DCE) + * ```json + * { + * "mode": "shake", + * "libraries": { + * "@their/library": { + * "functions": ["useExperimentalFlags"] + * } + * }, + * "flagValues": { + * "featureA": true, + * "featureB": false + * } + * } + * ``` + */ + export interface FeatureFlagsConfig { + /** + * Transform mode + * + * - **mark** (default): Marker-based - replaces flags with `__SWC_FLAGS__.flagName` + * for later substitution + * - **shake**: Direct optimization - substitutes flags with boolean values + * and performs DCE immediately + * + * @default "mark" + */ + mode?: TransformMode; + + /** + * Library configurations: library name -> config + * + * The plugin will track imports from these libraries and + * transform calls to the specified functions. + * + * @example + * { + * "@their/library": { + * functions: ["useExperimentalFlags"] + * }, + * "@another/flags": { + * functions: ["useFeatures"] + * } + * } + */ + libraries: Record; + + /** + * Flags to exclude from transformation + * + * These flags will not be transformed and will remain as-is. + * Useful for flags that don't need optimization. + * + * @default [] + */ + excludeFlags?: string[]; + + /** + * Global object name for markers + * + * Only used in mark mode. The plugin will replace flag identifiers + * with member expressions on this global object. + * + * @default "__SWC_FLAGS__" + */ + markerObject?: string; + + /** + * Flag values to apply (flag_name -> boolean) + * + * Required in shake mode. Maps flag names to their boolean values. + * The plugin will substitute these values directly and eliminate dead code. + * + * Not used in mark mode. + * + * @example + * { + * "featureA": true, + * "featureB": false, + * "experimentalUI": true + * } + */ + flagValues?: Record; + + /** + * Whether to collect transformation statistics + * + * Only used in shake mode. When enabled, the plugin tracks how many + * flags were processed and how much code was eliminated. + * + * @default true + */ + collectStats?: boolean; + } + /** * Build-time configuration for the feature flags plugin + * + * @deprecated Use FeatureFlagsConfig with mode: "shake" instead */ export interface BuildTimeConfig { /** @@ -64,16 +189,42 @@ declare module "@swc/plugin-experimental-feature-flags" { /** * Example usage in .swcrc: * + * Mark mode (marker generation for later substitution): + * ```json + * { + * "jsc": { + * "experimental": { + * "plugins": [ + * ["@swc/plugin-experimental-feature-flags", { + * "mode": "mark", + * "libraries": { + * "@their/library": { + * "functions": ["useExperimentalFlags"] + * } + * } + * }] + * ] + * } + * } + * } + * ``` + * + * Shake mode (direct optimization with DCE): * ```json * { * "jsc": { * "experimental": { * "plugins": [ * ["@swc/plugin-experimental-feature-flags", { + * "mode": "shake", * "libraries": { * "@their/library": { * "functions": ["useExperimentalFlags"] * } + * }, + * "flagValues": { + * "featureA": true, + * "featureB": false * } * }] * ]