fix(runtime): ensure lua data is initialized (#13010) e27bf13d74

Co-authored-by: hernan <hernan@rive.app>
diff --git a/.rive_head b/.rive_head
index 950e8f5..9be0836 100644
--- a/.rive_head
+++ b/.rive_head
@@ -1 +1 @@
-afd68206e676b8b4d360a720a86068094740d330
+e27bf13d74952314871275f26d0ed7228b97b4dc
diff --git a/include/rive/lua/rive_lua_libs.hpp b/include/rive/lua/rive_lua_libs.hpp
index e13a885..90943f6 100644
--- a/include/rive/lua/rive_lua_libs.hpp
+++ b/include/rive/lua/rive_lua_libs.hpp
@@ -1482,6 +1482,10 @@
     virtual void printEndLine() = 0;
     virtual int pCall(lua_State* state, int nargs, int nresults) = 0;
 
+    // When true, the VM's owner sets up the Lua `Data` global itself (the
+    // editor builds it in Dart), so File should not call initializeLuaData.
+    virtual bool initializesDataGlobalExternally() const { return false; }
+
     // Add a module to be registered later via performRegistration()
     void addModule(ModuleDetails* moduleDetails);
     // Perform registration of all added modules, handling dependencies and
diff --git a/src/file.cpp b/src/file.cpp
index 138465c..9daec55 100644
--- a/src/file.cpp
+++ b/src/file.cpp
@@ -693,6 +693,14 @@
         ScriptingVM* vm = m_scriptingVM.get();
         if (vm != nullptr)
         {
+            // Set up the Data global (view model constructors) on the active
+            // VM, whether it was created here or supplied externally (e.g. by
+            // the CommandServer). Skip it when the VM's owner builds Data
+            // itself, as the editor does in Dart.
+            if (!vm->context()->initializesDataGlobalExternally())
+            {
+                initializeLuaData(vm->state(), m_ViewModels);
+            }
             for (auto scriptAsset : scripts)
             {
                 // At runtime, if the script is verified, add it to be
@@ -729,7 +737,6 @@
     cleanupScriptingVM();
     auto context = std::make_unique<CPPRuntimeScriptingContext>(m_factory);
     m_scriptingVM = make_rcp<ScriptingVM>(std::move(context));
-    initializeLuaData(m_scriptingVM->state(), m_ViewModels);
 }
 
 lua_State* File::scriptingState()
diff --git a/tests/unit_tests/assets/data_global_repro.riv b/tests/unit_tests/assets/data_global_repro.riv
new file mode 100644
index 0000000..2ee24f5
--- /dev/null
+++ b/tests/unit_tests/assets/data_global_repro.riv
Binary files differ
diff --git a/tests/unit_tests/runtime/scripting/scripting_context_test.cpp b/tests/unit_tests/runtime/scripting/scripting_context_test.cpp
index dd5ecd0..1f20a7f 100644
--- a/tests/unit_tests/runtime/scripting/scripting_context_test.cpp
+++ b/tests/unit_tests/runtime/scripting/scripting_context_test.cpp
@@ -237,6 +237,44 @@
     CHECK(silver.matches("script_create_viewmodel_instance"));
 }
 
+// Regression test for rive-ios#454: when a ScriptingVM is supplied to
+// File::import (as the CommandServer does on iOS), the Lua `Data` global —
+// which exposes view model constructors like Data.ProbeChipVM.new() — must
+// still be initialized. Before the fix, initializeLuaData only ran inside
+// makeScriptingVM(), which is skipped when an external VM is provided, so
+// `Data` was nil in scripts.
+TEST_CASE("Data global is initialized when a ScriptingVM is provided to import",
+          "[scripting]")
+{
+    // Mirror CommandServer::processCommands: create the VM up front and pass
+    // it into File::import.
+    auto context =
+        std::make_unique<rive::CPPRuntimeScriptingContext>(&gNoOpFactory);
+    auto vm = rive::make_rcp<rive::ScriptingVM>(std::move(context));
+
+    auto bytes = ReadFile("assets/data_global_repro.riv");
+    rive::ImportResult result;
+    auto file =
+        rive::File::import(bytes, &gNoOpFactory, &result, nullptr, vm.get());
+    REQUIRE(result == rive::ImportResult::success);
+    REQUIRE(file != nullptr);
+
+    // The file adopts the VM we supplied...
+    REQUIRE(file->scriptingVM() == vm.get());
+
+    // ...and its `Data` global is a populated table (was nil before the fix),
+    // exposing the file's view model as a constructor.
+    lua_State* L = vm->state();
+    REQUIRE(L != nullptr);
+    lua_getglobal(L, "Data");
+    REQUIRE(lua_istable(L, -1));
+    lua_getfield(L, -1, "ProbeChipVM");
+    REQUIRE(lua_istable(L, -1));
+    lua_getfield(L, -1, "new");
+    CHECK(lua_isfunction(L, -1));
+    lua_pop(L, 3);
+}
+
 TEST_CASE("script has access to the data bound view model", "[silver]")
 {
     rive::SerializingFactory silver;