Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions crates/swc-react-native-worklets/src/options.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
30 changes: 29 additions & 1 deletion crates/swc-react-native-worklets/src/visitor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -64,6 +67,7 @@ pub struct WorkletsVisitor {
pub globals: FxHashSet<Atom>,
pub file_bindings: FxHashSet<Atom>,
pub source_map: Option<Lrc<SourceMap>>,
unsupported_bundle_mode_reported: bool,
pending_prepends: Vec<Stmt>,
}

Expand All @@ -85,6 +89,7 @@ impl WorkletsVisitor {
globals,
file_bindings: FxHashSet::default(),
source_map: None,
unsupported_bundle_mode_reported: false,
pending_prepends: vec![],
}
}
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
110 changes: 51 additions & 59 deletions crates/swc-react-native-worklets/tests/transform_test.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Mutex<Vec<String>>>,
}

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() {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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 <Foo />;
}
"#;
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#"
Expand Down
Loading