Skip to content
Closed
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
2 changes: 2 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ option(JSRUNTIMEHOST_POLYFILL_FILE "Include JsRuntimeHost Polyfill File and File
option(JSRUNTIMEHOST_POLYFILL_PERFORMANCE "Include JsRuntimeHost Polyfill Performance." ON)
option(JSRUNTIMEHOST_POLYFILL_TEXTDECODER "Include JsRuntimeHost Polyfill TextDecoder." ON)
option(JSRUNTIMEHOST_POLYFILL_TEXTENCODER "Include JsRuntimeHost Polyfill TextEncoder." ON)
option(JSRUNTIMEHOST_POLYFILL_IMPORT_SCRIPTS "Include JsRuntimeHost Polyfill importScripts." ON)
option(JSRUNTIMEHOST_POLYFILL_DYNAMIC_SCRIPT_LOADER "Include JsRuntimeHost asynchronous script loader." ON)

# Sanitizers
option(ENABLE_SANITIZERS "Enable AddressSanitizer and UBSan" OFF)
Expand Down
8 changes: 8 additions & 0 deletions Polyfills/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,12 @@ endif()

if(JSRUNTIMEHOST_POLYFILL_TEXTENCODER)
add_subdirectory(TextEncoder)
endif()

if(JSRUNTIMEHOST_POLYFILL_IMPORT_SCRIPTS)
add_subdirectory(ImportScripts)
endif()

if(JSRUNTIMEHOST_POLYFILL_DYNAMIC_SCRIPT_LOADER)
add_subdirectory(DynamicScriptLoader)
endif()
15 changes: 15 additions & 0 deletions Polyfills/DynamicScriptLoader/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
set(SOURCES
"Include/Babylon/Polyfills/DynamicScriptLoader.h"
"Source/DynamicScriptLoader.cpp")

add_library(DynamicScriptLoader ${SOURCES})
warnings_as_errors(DynamicScriptLoader)

target_include_directories(DynamicScriptLoader PUBLIC "Include")

target_link_libraries(DynamicScriptLoader
PUBLIC Foundation
PUBLIC napi)

set_property(TARGET DynamicScriptLoader PROPERTY FOLDER Polyfills)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include <Babylon/Api.h>
#include <napi/env.h>

#include <functional>
#include <string>

namespace Babylon::Polyfills::DynamicScriptLoader
{
// Return source as a JavaScript string or a Promise resolving to one.
// Return null/undefined (or reject) when the resource is unavailable.
// Called on the JavaScript thread; any asynchronous completion must also use
// the runtime dispatcher before accessing Node-API values.
using ResolverT = std::function<Napi::Value BABYLON_API (Napi::Env, const std::string&)>;

// Installs loadScript(name): Promise<void>. The resolver alone determines
// which resources can be loaded; no file or network fallback is provided.
void BABYLON_API Initialize(Napi::Env env, ResolverT resolver);
}
60 changes: 60 additions & 0 deletions Polyfills/DynamicScriptLoader/Source/DynamicScriptLoader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include <Babylon/Polyfills/DynamicScriptLoader.h>

#include <stdexcept>
#include <utility>

namespace Babylon::Polyfills::DynamicScriptLoader
{
void BABYLON_API Initialize(Napi::Env env, ResolverT resolver)
{
if (!resolver)
{
throw std::invalid_argument{"loadScript requires a resource resolver"};
}

auto global = env.Global();
if (!global.Get("loadScript").IsUndefined())
{
throw Napi::Error::New(env, "loadScript is already defined");
}

global.Set("loadScript",
Napi::Function::New(env, [resolver = std::move(resolver)](const Napi::CallbackInfo& info) -> Napi::Value {
auto env = info.Env();
const auto deferred = Napi::Promise::Deferred::New(env);
if (info.Length() != 1 || !info[0].IsString())
{
deferred.Reject(Napi::TypeError::New(env, "loadScript expects one resource name").Value());
return deferred.Promise();
}

const auto name = info[0].As<Napi::String>().Utf8Value();
try
{
auto source = resolver(env, name);
auto promise = env.Global().Get("Promise").As<Napi::Function>();
auto resolve = promise.Get("resolve").As<Napi::Function>();
auto resolved = resolve.Call(promise, {source}).As<Napi::Promise>();
auto evaluate = Napi::Function::New(env, [name](const Napi::CallbackInfo& callback) {
if (!callback[0].IsString())
{
throw Napi::TypeError::New(callback.Env(), "Script source not found: " + name);
}
auto text = callback[0].As<Napi::String>().Utf8Value();
Napi::Eval(callback.Env(), text.c_str(), name.c_str());
});
return resolved.Get("then").As<Napi::Function>().Call(resolved, {evaluate});
}
catch (const Napi::Error& error)
{
deferred.Reject(error.Value());
}
catch (const std::exception& error)
{
deferred.Reject(Napi::Error::New(env, error.what()).Value());
}
return deferred.Promise();
},
"loadScript"));
}
}
15 changes: 15 additions & 0 deletions Polyfills/ImportScripts/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
set(SOURCES
"Include/Babylon/Polyfills/ImportScripts.h"
"Source/ImportScripts.cpp")

add_library(ImportScripts ${SOURCES})
warnings_as_errors(ImportScripts)

target_include_directories(ImportScripts PUBLIC "Include")

target_link_libraries(ImportScripts
PUBLIC Foundation
PUBLIC napi)

set_property(TARGET ImportScripts PROPERTY FOLDER Polyfills)
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES})
20 changes: 20 additions & 0 deletions Polyfills/ImportScripts/Include/Babylon/Polyfills/ImportScripts.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#pragma once

#include <Babylon/Api.h>
#include <napi/env.h>

#include <functional>
#include <optional>
#include <string>

namespace Babylon::Polyfills::ImportScripts
{
// Return source for an explicitly packaged script, or nullopt when it is unavailable.
// The resolver runs synchronously on the JavaScript thread and must not load arbitrary files.
using ResolverT = std::function<std::optional<std::string> BABYLON_API (const std::string&)>;

// Install importScripts(...names) for bundles split by a bundler. Resource
// names must be strings; each is evaluated in order in the current context.
// This does not enable native ES module syntax in engines such as Chakra.
void BABYLON_API Initialize(Napi::Env env, ResolverT resolver);
}
60 changes: 60 additions & 0 deletions Polyfills/ImportScripts/Source/ImportScripts.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#include <Babylon/Polyfills/ImportScripts.h>

#include <stdexcept>
#include <utility>

namespace Babylon::Polyfills::ImportScripts
{
void BABYLON_API Initialize(Napi::Env env, ResolverT resolver)
{
if (!resolver)
{
throw std::invalid_argument{"importScripts requires a resource resolver"};
}

auto global = env.Global();
if (!global.Get("importScripts").IsUndefined())
{
throw Napi::Error::New(env, "importScripts is already defined");
}

if (global.Get("self").IsUndefined())
{
global.Set("self", global);
}

global.Set("importScripts",
Napi::Function::New(env, [resolver = std::move(resolver)](const Napi::CallbackInfo& info) {
for (size_t index = 0; index < info.Length(); ++index)
{
if (!info[index].IsString())
{
throw Napi::TypeError::New(info.Env(), "importScripts expects resource names as strings");
}

const auto name = info[index].As<Napi::String>().Utf8Value();
std::optional<std::string> source;
try
{
source = resolver(name);
}
catch (const Napi::Error&)
{
throw;
}
catch (const std::exception& error)
{
throw Napi::Error::New(info.Env(), error.what());
}

if (!source)
{
throw Napi::Error::New(info.Env(), "Embedded script not found: " + name);
}

Napi::Eval(info.Env(), source->c_str(), name.c_str());
}
},
"importScripts"));
}
}
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,58 @@ polyfills that consumers can include if required.
> not supported on Apple platforms (iOS or macOS)** — configuring the build with
> `NAPI_JAVASCRIPT_ENGINE=Hermes` on those targets will fail with a CMake error.

## Dynamic JavaScript loading

The optional `DynamicScriptLoader` polyfill installs `loadScript(name)`, which
returns a promise that settles after evaluating source in the current JavaScript
context. The host supplies a resolver that returns either a JavaScript string or
a promise of a string, so resources may be available immediately or retrieved
asynchronously. Missing resources (null or undefined), rejected lookups, and
evaluation errors reject the returned promise. The resolver runs on the
JavaScript thread; if an asynchronous host operation finishes on another thread,
dispatch back to the JavaScript thread before resolving its promise. No file or
network loader is installed by JsRuntimeHost.

```cpp
#include <Babylon/Polyfills/DynamicScriptLoader.h>

Babylon::Polyfills::DynamicScriptLoader::Initialize(env,
[](Napi::Env env, const std::string& name) -> Napi::Value {
auto source = FindPackagedScript(name);
return source ? Napi::String::New(env, *source) : env.Null();
});
```

For hosts with synchronous packaged resources, the optional `ImportScripts`
polyfill provides a worker-style `importScripts(...names)` loader. Install it before
evaluating the entry script, and return source only for exact packaged names:

```cpp
#include <Babylon/Polyfills/ImportScripts.h>

Babylon::Polyfills::ImportScripts::Initialize(env,
[](const std::string& name) -> std::optional<std::string> {
return FindEmbeddedChunk(name); // nullopt for names not packaged by the host
});
```

This loader also exposes `self` if absent. It accepts string resource names
and evaluates them synchronously in order; missing chunks and evaluation
failures throw JavaScript errors. Both resolvers
control which names are accepted; neither polyfill adds a file or network
fallback. The `JSRUNTIMEHOST_POLYFILL_DYNAMIC_SCRIPT_LOADER` and
`JSRUNTIMEHOST_POLYFILL_IMPORT_SCRIPTS` options control the respective libraries.

For source-level `import("./chunk")`, configure the native Webpack build with
`output.chunkLoading: "import-scripts"` and `output.chunkFormat: "array-push"`,
then package its emitted chunks for the resolver. Webpack still returns a promise
from `import()`; its native loader calls `importScripts` to install a chunk before
resolving that promise. Hosts with only asynchronous resource access instead need
a bundler chunk loader that awaits `loadScript(name)`; Webpack's built-in
`import-scripts` loader cannot await it. A browser build can use its normal
asynchronous URL-based chunk loader. Neither polyfill adds native ES module parsing
to engines such as Chakra.


## **Building - All Development Platforms**

Expand Down
4 changes: 4 additions & 0 deletions Tests/UnitTests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ set(SOURCES
"Source/Tests.AppRuntime.cpp"
"Source/Tests.Console.cpp"
"Source/Tests.DelayedTaskScheduler.cpp"
"Source/Tests.DynamicScriptLoader.cpp"
"Source/Tests.JavaScript.cpp"
"Source/Tests.ImportScripts.cpp"
"Source/Tests.NodeApi.cpp"
"Source/Tests.Scheduling.cpp"
"Source/Tests.StandardStreamLogger.cpp"
Expand Down Expand Up @@ -79,6 +81,8 @@ endif()

target_link_libraries(UnitTests
PRIVATE AppRuntime
PRIVATE DynamicScriptLoader
PRIVATE ImportScripts
PRIVATE Console
PRIVATE AbortController
PRIVATE SchedulingInternal
Expand Down
86 changes: 86 additions & 0 deletions Tests/UnitTests/Source/Tests.DynamicScriptLoader.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
#include <Babylon/AppRuntime.h>
#include <Babylon/JsRuntime.h>
#include <Babylon/Polyfills/DynamicScriptLoader.h>
#include <Babylon/ScriptLoader.h>
#include <gtest/gtest.h>

#include <chrono>
#include <future>
#include <stdexcept>
#include <string>

TEST(DynamicScriptLoader, SupportsImmediateAndDeferredResources)
{
std::promise<std::string> result;
Babylon::AppRuntime runtime{};
runtime.Dispatch([&result](Napi::Env env) {
Babylon::Polyfills::DynamicScriptLoader::Initialize(env, [](Napi::Env env, const std::string& name) -> Napi::Value {
if (name == "immediate.js")
{
return Napi::String::New(env, "window.loaded = 1;");
}
if (name == "deferred.js")
{
auto deferred = Napi::Promise::Deferred::New(env);
Babylon::JsRuntime::GetFromJavaScript(env).Dispatch([deferred](Napi::Env callbackEnv) {
deferred.Resolve(Napi::String::New(callbackEnv, "window.loaded += 41;"));
});
return deferred.Promise();
}
return env.Null();
});
env.Global().Set("reportResult", Napi::Function::New(env, [&result](const Napi::CallbackInfo& info) {
result.set_value(info[0].As<Napi::String>().Utf8Value());
}));
});

Babylon::ScriptLoader loader{runtime};
loader.Eval(R"(
loadScript("immediate.js")
.then(function () { return loadScript("deferred.js"); })
.then(function () { return loadScript("missing.js"); })
.then(function () { reportResult("missing script loaded"); },
function (error) { reportResult(window.loaded + "|" + error.message); })
.catch(function (error) { reportResult("unexpected: " + error.message); });
)", "entry.js");

auto future = result.get_future();
ASSERT_EQ(future.wait_for(std::chrono::seconds(10)), std::future_status::ready);
EXPECT_EQ(future.get(), "42|Script source not found: missing.js");
}

TEST(DynamicScriptLoader, RejectsResolverAndScriptErrors)
{
std::promise<std::string> result;
Babylon::AppRuntime runtime{};
runtime.Dispatch([&result](Napi::Env env) {
Babylon::Polyfills::DynamicScriptLoader::Initialize(env, [](Napi::Env env, const std::string& name) -> Napi::Value {
if (name == "broken.js")
{
return Napi::String::New(env, "throw new Error('script failed')");
}
if (name == "rejected.js")
{
return Napi::Eval(env, "Promise.reject(new Error('lookup failed'))", "resolver.js");
}
throw std::runtime_error{"resolver failed"};
});
env.Global().Set("reportResult", Napi::Function::New(env, [&result](const Napi::CallbackInfo& info) {
result.set_value(info[0].As<Napi::String>().Utf8Value());
}));
});

Babylon::ScriptLoader loader{runtime};
loader.Eval(R"(
Promise.all([
loadScript("broken.js").then(null, function (error) { return error.message; }),
loadScript("rejected.js").then(null, function (error) { return error.message; }),
loadScript("throw.js").then(null, function (error) { return error.message; }),
loadScript().then(null, function (error) { return error.name; })
]).then(function (errors) { reportResult(errors.join("|")); });
)", "entry.js");

auto future = result.get_future();
ASSERT_EQ(future.wait_for(std::chrono::seconds(10)), std::future_status::ready);
EXPECT_EQ(future.get(), "script failed|lookup failed|resolver failed|TypeError");
}
Loading
Loading