diff --git a/crates/swc-react-native-worklets/src/options.rs b/crates/swc-react-native-worklets/src/options.rs index 0b7ea57..dc5f2a9 100644 --- a/crates/swc-react-native-worklets/src/options.rs +++ b/crates/swc-react-native-worklets/src/options.rs @@ -49,6 +49,10 @@ pub struct WorkletsOptions { pub disable_inline_styles_warning: bool, /// Enable Bundle Mode. + /// + /// Kept for config compatibility with the upstream Babel plugin, but the + /// current Rust port does not support Bundle Mode. Enabling it reports an + /// SWC diagnostic and leaves the input unchanged. pub bundle_mode: bool, /// Filename of the file being transformed (used for source map output and @@ -82,9 +86,7 @@ pub struct WorkletsOptions { /// Bundle Mode import-forwarding options. /// - /// Stored for API parity. The current Rust port does not yet emit - /// generated worklet files, so this only preserves the latest option - /// shape for callers. + /// Stored for API parity while Bundle Mode remains unsupported. pub import_forwarding: ImportForwardingOptions, /// Deprecated compatibility field for the removed upstream diff --git a/crates/swc-react-native-worklets/src/visitor.rs b/crates/swc-react-native-worklets/src/visitor.rs index b5df644..39a1d76 100644 --- a/crates/swc-react-native-worklets/src/visitor.rs +++ b/crates/swc-react-native-worklets/src/visitor.rs @@ -8,8 +8,9 @@ use std::path::{Path, PathBuf}; use rustc_hash::FxHashSet; use swc_atoms::Atom; +use swc_common::errors::HANDLER; use swc_common::source_map::DefaultSourceMapGenConfig; -use swc_common::{sync::Lrc, BytePos, LineCol, SourceMap, SyntaxContext, DUMMY_SP}; +use swc_common::{sync::Lrc, BytePos, LineCol, SourceMap, Span, SyntaxContext, DUMMY_SP}; use swc_ecma_ast::*; use swc_ecma_codegen::{text_writer::JsWriter, Config as EmitConfig, Emitter}; use swc_ecma_utils::ExprFactory; @@ -49,6 +50,8 @@ const CONTEXT_OBJECT_MARKER: &str = "__workletContextObject"; const CONTEXT_OBJECT_FACTORY: &str = "__workletContextObjectFactory"; const WORKLET_CLASS_MARKER: &str = "__workletClass"; const GENERATED_WORKLETS_DIR: &str = ".worklets"; +const BUNDLE_MODE_UNSUPPORTED_MESSAGE: &str = + "react-native-worklets bundleMode is not supported by swc-react-native-worklets"; /// Suffix appended to the original class name to form the factory function /// identifier. Matches the upstream babel plugin's @@ -64,6 +67,7 @@ pub struct WorkletsVisitor { pub globals: FxHashSet, pub file_bindings: FxHashSet, pub source_map: Option>, + unsupported_bundle_mode_reported: bool, pending_prepends: Vec, } @@ -85,6 +89,7 @@ impl WorkletsVisitor { globals, file_bindings: FxHashSet::default(), source_map: None, + unsupported_bundle_mode_reported: false, pending_prepends: vec![], } } @@ -113,6 +118,21 @@ impl WorkletsVisitor { } } + fn report_unsupported_bundle_mode(&mut self, span: Span) { + if self.unsupported_bundle_mode_reported { + return; + } + self.unsupported_bundle_mode_reported = true; + + if HANDLER.is_set() { + HANDLER.with(|handler| { + handler + .struct_span_err(span, BUNDLE_MODE_UNSUPPORTED_MESSAGE) + .emit() + }); + } + } + /// Path to emit into `init_data.location` (and as the `sources[0]` entry /// of emitted source maps). Honors `relative_source_location` by stripping /// the cwd prefix when possible (caller can override cwd via options). @@ -1144,6 +1164,10 @@ impl WorkletsVisitor { impl VisitMut for WorkletsVisitor { fn visit_mut_module(&mut self, module: &mut Module) { + if self.options.bundle_mode { + self.report_unsupported_bundle_mode(module.span); + return; + } if self.skip_file { return; } @@ -1182,6 +1206,10 @@ impl VisitMut for WorkletsVisitor { } fn visit_mut_script(&mut self, script: &mut Script) { + if self.options.bundle_mode { + self.report_unsupported_bundle_mode(script.span); + return; + } if self.skip_file { return; } diff --git a/crates/swc-react-native-worklets/tests/transform_test.rs b/crates/swc-react-native-worklets/tests/transform_test.rs index a77fa28..5cbc482 100644 --- a/crates/swc-react-native-worklets/tests/transform_test.rs +++ b/crates/swc-react-native-worklets/tests/transform_test.rs @@ -8,7 +8,21 @@ mod common; +use std::sync::{Arc, Mutex}; + use common::{options_with_version, transform_fixture, transform_fixture_resolved}; +use swc_common::errors::{emitter::Emitter, DiagnosticBuilder, Handler, HANDLER}; +use swc_react_native_worklets::WorkletsOptions; + +struct RecordingEmitter { + messages: Arc>>, +} + +impl Emitter for RecordingEmitter { + fn emit(&mut self, db: &mut DiagnosticBuilder<'_>) { + self.messages.lock().unwrap().push(db.message()); + } +} #[test] fn no_worklet_directive_passes_through() { @@ -214,33 +228,6 @@ class Sky { insta::assert_snapshot!(out); } -#[test] -fn worklet_class_marker_skipped_in_bundle_mode() { - let code = r#" -class Sky { - __workletClass = true; - draw() { this.x = 1; } -} -"#; - let mut opts = options_with_version(); - opts.bundle_mode = true; - let out = transform_fixture("Sample.ts", code, opts); - - // Bundle mode mirrors babel's `bundleMode`: `processIfWorkletClass` - // returns early, so the class is left fully untouched — marker - // intact, no method-level workletization triggered by the marker, - // no factory emitted. - assert_not_contains(&out, "Sky__classFactory"); - assert_contains(&out, "__workletClass"); - // The plain `draw()` method must NOT have been workletized (no - // factory call shape) because the marker pathway is the only thing - // that should opt this class in. - assert_not_contains(&out, "drawFactory"); - assert_not_contains(&out, "__workletHash"); - - insta::assert_snapshot!(out); -} - #[test] fn worklet_class_marker_without_methods_still_wraps() { // A class can opt-in to the worklet-class machinery without having @@ -297,23 +284,51 @@ function fn() { } #[test] -fn bundle_mode_omits_native_only_data_and_stack_details() { +fn bundle_mode_option_is_accepted_but_reports_error_and_noops() { + let opts: WorkletsOptions = serde_json::from_value(serde_json::json!({ + "bundleMode": true, + "importForwarding": { + "moduleNames": ["react-native-worklets"], + "relativePaths": ["./worklets"] + }, + "pluginVersion": "test" + })) + .expect("bundleMode options should deserialize"); + + assert!(opts.bundle_mode); + assert_eq!( + opts.import_forwarding.module_names, + ["react-native-worklets"] + ); + assert_eq!(opts.import_forwarding.relative_paths, ["./worklets"]); + let code = r#" function fn() { 'worklet'; return 1; } "#; - let mut opts = options_with_version(); - opts.bundle_mode = true; - let out = transform_fixture("Sample.ts", code, opts); + let messages = Arc::new(Mutex::new(Vec::new())); + let handler = Handler::with_emitter( + true, + false, + Box::new(RecordingEmitter { + messages: messages.clone(), + }), + ); + let out = HANDLER.set(&handler, || transform_fixture("Sample.ts", code, opts)); - assert_contains(&out, "__workletHash"); - assert_contains(&out, "__pluginVersion"); - assert_not_contains(&out, "_init_data"); + assert_eq!(handler.err_count(), 1); + let messages = messages.lock().unwrap(); + assert_eq!(messages.len(), 1); + assert!( + messages[0].contains("bundleMode is not supported"), + "unexpected error: {}", + messages[0] + ); + assert_contains(&out, "worklet"); + assert_not_contains(&out, "__workletHash"); assert_not_contains(&out, "__initData"); - assert_not_contains(&out, "__stackDetails"); - assert_not_contains(&out, "new global.Error"); } #[test] @@ -752,29 +767,6 @@ function fn(): any { ); } -#[test] -fn bundle_mode_jsx_tag_is_captured() { - let code = r#" -import { Foo } from './foo'; - -function fn(): any { - 'worklet'; - return ; -} -"#; - let mut opts = options_with_version(); - opts.bundle_mode = true; - let out = transform_fixture("Sample.tsx", code, opts); - - let destructure = extract_factory_iife_destructure(&out, "fn") - .expect("fn factory IIFE destructuring should be emitted"); - - assert!( - ident_in_destructure(&destructure, "Foo"), - "Foo (JSX tag) should be captured in bundle mode, got: {destructure}" - ); -} - #[test] fn deprecated_fabric_global_is_captured() { let code = r#"