-
Notifications
You must be signed in to change notification settings - Fork 23
Add File / FileReader polyfill #169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
bkaradzic-microsoft
merged 13 commits into
BabylonJS:main
from
bkaradzic-microsoft:weekend/tpc-1582-file-polyfill
Jun 5, 2026
Merged
Changes from all commits
Commits
Show all changes
13 commits
Select commit
Hold shift + click to select a range
4d29e69
Add File / FileReader polyfill
bkaradzic afcd679
fix(File): revert to `throw Napi::TypeError`; comment-out throw test …
bkaradzic 13b026b
review(File): drop readAsBinaryString, File extends Blob, drop shared…
bkaradzic-microsoft fb73ee9
fix(File): JSC prototype-chain wire-up via temp-instance trick
bkaradzic-microsoft 68e305c
File: try direct setPrototypeOf first, fall back to temp-instance
bkaradzic-microsoft 148877c
File: do prototype-chain wire-up in JS so JSC errors stay caught
bkaradzic-microsoft e34af33
File polyfill: address non-controversial review nits from bghgary on …
bkaradzic-microsoft be71aed
File polyfill: drop JS_PROTOTYPE_CHAIN_SHIM workaround now that #177 …
bkaradzic-microsoft fe457e5
File: drop #177 historical paragraph from setPrototypeOf comment
bkaradzic-microsoft eaeaf20
FileReader: back readonly + on* attributes with C++ state via Instanc…
bkaradzic-microsoft a6155da
FileReader: encode data: URLs via base-n instead of a bespoke base64 …
bkaradzic-microsoft 14122f1
FileReader: box result/error so string results survive real N-API
bkaradzic-microsoft 61d9924
FileReader: anchor in-flight read via lambda-owned ref, not a self-cycle
bkaradzic File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,18 @@ | ||
| set(SOURCES | ||
| "Include/Babylon/Polyfills/File.h" | ||
| "Source/File.h" | ||
| "Source/File.cpp" | ||
| "Source/FileReader.h" | ||
| "Source/FileReader.cpp") | ||
|
|
||
| add_library(File ${SOURCES}) | ||
| warnings_as_errors(File) | ||
|
|
||
| target_include_directories(File PUBLIC "Include") | ||
|
|
||
| target_link_libraries(File | ||
| PRIVATE base-n | ||
| PUBLIC JsRuntime) | ||
|
|
||
| set_property(TARGET File PROPERTY FOLDER Polyfills) | ||
| source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${SOURCES}) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| #pragma once | ||
|
|
||
| #include <napi/env.h> | ||
| #include <Babylon/Api.h> | ||
|
|
||
| namespace Babylon::Polyfills::File | ||
| { | ||
| void BABYLON_API Initialize(Napi::Env env); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,35 @@ | ||
| # File | ||
|
|
||
| Implements the `File` and `FileReader` web APIs on top of the native `Blob` | ||
| polyfill provided by JsRuntimeHost. Babylon.js GLTF/OBJ serializer | ||
| round-trip codepaths construct `new File([blob], 'scene.glb')` and read it | ||
| back via `FileReader.readAsArrayBuffer(...)`, so the runtime needs both | ||
| constructors for those tests (and any consumer code that wraps serializer | ||
| output) to work. | ||
|
|
||
| ## Behaviour | ||
|
|
||
| * No-op when the runtime already exposes a global `File` / `FileReader` | ||
| (e.g. V8 in some embeddings). | ||
| * `File` is self-contained: the constructor delegates to the global | ||
| `Blob` constructor to build the underlying byte storage, then decorates | ||
| the instance with `name` and `lastModified`. Methods (`size`, `type`, | ||
| `arrayBuffer`, `text`, `bytes`) forward to the inner `Blob`. | ||
| * `FileReader` supports `readAsArrayBuffer`, `readAsText`, and | ||
| `readAsDataURL`, plus `abort`, `addEventListener` / | ||
| `removeEventListener` / `dispatchEvent`, and the standard `onload` / | ||
| `onerror` / `onloadstart` / `onloadend` / `onprogress` / `onabort` | ||
| handler slots. `abort()` invalidates in-flight reads via a monotonic | ||
| token so late-resolving `arrayBuffer()` promises cannot dispatch a | ||
| phantom `load` event after a user-initiated abort. | ||
| * `File` extends `Blob`: the JS-visible prototype chain is wired so | ||
| `new File(...) instanceof Blob === true`. Babylon.js core branches on | ||
| `instanceof Blob` in several places (fileTools, Offline/database, | ||
| abstractEngine, thinNativeEngine). | ||
|
|
||
| ## Prerequisites | ||
|
|
||
| `Babylon::Polyfills::Blob::Initialize(env)` (from JsRuntimeHost) must be | ||
| called before `Babylon::Polyfills::File::Initialize(env)`; if `Blob` is | ||
| missing from the global object when `File::Initialize` runs, the `File` | ||
| constructor will not be registered. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| #include "File.h" | ||
| #include "FileReader.h" | ||
|
|
||
| #include <Babylon/Polyfills/File.h> | ||
|
|
||
| #include <chrono> | ||
| #include <string> | ||
|
|
||
| namespace Babylon::Polyfills::Internal | ||
| { | ||
| namespace | ||
| { | ||
| constexpr auto JS_FILE_CONSTRUCTOR_NAME = "File"; | ||
| constexpr auto JS_BLOB_CONSTRUCTOR_NAME = "Blob"; | ||
| } | ||
|
|
||
| void File::Initialize(Napi::Env env) | ||
| { | ||
| auto global = env.Global(); | ||
|
|
||
| // No-op if the runtime already provides a global File. Cheapest | ||
| // check, and the common path on platforms with a native File. | ||
| if (!global.Get(JS_FILE_CONSTRUCTOR_NAME).IsUndefined()) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| // Require the native Blob polyfill: File delegates byte storage to | ||
| // a Blob, so without it the constructor cannot produce useful | ||
| // instances. Use IsUndefined() rather than IsFunction() because | ||
| // some JavaScriptCore builds (notably libjavascriptcoregtk on | ||
| // Linux) classify constructors created via JSObjectMakeConstructor | ||
| // as typeof 'object', not 'function', so napi_typeof returns | ||
| // napi_object for them. | ||
| auto blob = global.Get(JS_BLOB_CONSTRUCTOR_NAME); | ||
| if (blob.IsUndefined() || blob.IsNull()) | ||
| { | ||
| throw Napi::Error::New(env, | ||
| "File polyfill requires the Blob polyfill to be installed first."); | ||
| } | ||
|
|
||
| Napi::Function func = DefineClass( | ||
| env, | ||
| JS_FILE_CONSTRUCTOR_NAME, | ||
| { | ||
| InstanceAccessor("size", &File::GetSize, nullptr), | ||
| InstanceAccessor("type", &File::GetType, nullptr), | ||
| InstanceAccessor("name", &File::GetName, nullptr), | ||
| InstanceAccessor("lastModified", &File::GetLastModified, nullptr), | ||
| InstanceMethod("arrayBuffer", &File::ArrayBuffer), | ||
| InstanceMethod("text", &File::Text), | ||
| InstanceMethod("bytes", &File::Bytes), | ||
| }); | ||
|
|
||
| global.Set(JS_FILE_CONSTRUCTOR_NAME, func); | ||
|
|
||
| // Wire File.prototype's [[Prototype]] to Blob.prototype so | ||
| // `new File(...) instanceof Blob === true`. WHATWG specs File as | ||
| // a Blob subtype; BJS core (fileTools, Offline/database, | ||
| // abstractEngine, thinNativeEngine) branches on `instanceof Blob` | ||
| // and needs File inputs to satisfy that check. | ||
| auto setPrototypeOf = env.Global().Get("Object").As<Napi::Object>() | ||
| .Get("setPrototypeOf").As<Napi::Function>(); | ||
| setPrototypeOf.Call({ | ||
| func.Get("prototype"), | ||
| blob.As<Napi::Function>().Get("prototype"), | ||
| }); | ||
| } | ||
|
|
||
| File::File(const Napi::CallbackInfo& info) | ||
| : Napi::ObjectWrap<File>{info} | ||
| { | ||
| auto env = info.Env(); | ||
|
|
||
| // The WHATWG File constructor takes (fileBits, fileName, [options]). | ||
| // Both fileBits and fileName are required (USVString without | ||
| // `optional`), so missing either is a TypeError per WebIDL bindings. | ||
| if (info.Length() < 2) | ||
| { | ||
| throw Napi::TypeError::New(env, | ||
| "Failed to construct 'File': 2 arguments required, but only " + | ||
| std::to_string(info.Length()) + " present."); | ||
| } | ||
|
|
||
| Napi::Value parts = info[0]; | ||
| Napi::Value name = info[1]; | ||
| Napi::Value options = info.Length() > 2 ? info[2] : env.Undefined(); | ||
|
|
||
| // USVString conversion: undefined -> "undefined", null -> "null", | ||
| // numbers/objects -> their .toString() representation. Napi::Value:: | ||
| // ToString() routes through napi_coerce_to_string, which matches | ||
| // these semantics on all three engines. | ||
| m_name = name.ToString().Utf8Value(); | ||
|
|
||
| // Default lastModified to the current wall clock in milliseconds, | ||
| // matching Date.now() semantics used by the JS File constructor. | ||
| m_lastModified = static_cast<double>( | ||
| std::chrono::duration_cast<std::chrono::milliseconds>( | ||
| std::chrono::system_clock::now().time_since_epoch()) | ||
| .count()); | ||
|
|
||
| auto blobOptions = Napi::Object::New(env); | ||
|
|
||
| if (options.IsObject()) | ||
| { | ||
| auto optsObj = options.As<Napi::Object>(); | ||
| if (optsObj.Has("type")) | ||
| { | ||
| blobOptions.Set("type", optsObj.Get("type")); | ||
| } | ||
| if (optsObj.Has("lastModified")) | ||
| { | ||
| auto lm = optsObj.Get("lastModified"); | ||
| if (lm.IsNumber()) | ||
| { | ||
| m_lastModified = lm.As<Napi::Number>().DoubleValue(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| Napi::Value partsArray; | ||
| if (parts.IsArray()) | ||
| { | ||
| partsArray = parts; | ||
| } | ||
| else | ||
| { | ||
| partsArray = Napi::Array::New(env, 0); | ||
| } | ||
|
|
||
| // Delegate byte-buffer construction to the native Blob polyfill so | ||
| // we benefit from its existing BlobPart handling (ArrayBuffer, | ||
| // typed array, string, Blob). | ||
| auto blobCtor = env.Global().Get(JS_BLOB_CONSTRUCTOR_NAME).As<Napi::Function>(); | ||
| auto blobInstance = blobCtor.New({partsArray, blobOptions}); | ||
| m_blob = Napi::Persistent(blobInstance); | ||
| } | ||
|
|
||
| Napi::Value File::GetSize(const Napi::CallbackInfo&) | ||
| { | ||
| return m_blob.Value().Get("size"); | ||
| } | ||
|
|
||
| Napi::Value File::GetType(const Napi::CallbackInfo&) | ||
| { | ||
| return m_blob.Value().Get("type"); | ||
| } | ||
|
|
||
| Napi::Value File::GetName(const Napi::CallbackInfo& info) | ||
| { | ||
| return Napi::String::New(info.Env(), m_name); | ||
| } | ||
|
|
||
| Napi::Value File::GetLastModified(const Napi::CallbackInfo& info) | ||
| { | ||
| return Napi::Number::New(info.Env(), m_lastModified); | ||
| } | ||
|
|
||
| Napi::Value File::ArrayBuffer(const Napi::CallbackInfo&) | ||
| { | ||
| auto blob = m_blob.Value(); | ||
| return blob.Get("arrayBuffer").As<Napi::Function>().Call(blob, {}); | ||
| } | ||
|
|
||
| Napi::Value File::Text(const Napi::CallbackInfo&) | ||
| { | ||
| auto blob = m_blob.Value(); | ||
| return blob.Get("text").As<Napi::Function>().Call(blob, {}); | ||
| } | ||
|
|
||
| Napi::Value File::Bytes(const Napi::CallbackInfo&) | ||
| { | ||
| auto blob = m_blob.Value(); | ||
| return blob.Get("bytes").As<Napi::Function>().Call(blob, {}); | ||
| } | ||
| } | ||
|
|
||
| namespace Babylon::Polyfills::File | ||
| { | ||
| void BABYLON_API Initialize(Napi::Env env) | ||
| { | ||
| Internal::File::Initialize(env); | ||
| Internal::FileReader::Initialize(env); | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| #pragma once | ||
|
|
||
| #include <napi/napi.h> | ||
|
|
||
| #include <string> | ||
|
|
||
| namespace Babylon::Polyfills::Internal | ||
| { | ||
| class File final : public Napi::ObjectWrap<File> | ||
| { | ||
| public: | ||
| static void Initialize(Napi::Env env); | ||
|
|
||
| explicit File(const Napi::CallbackInfo& info); | ||
|
|
||
| private: | ||
| Napi::Value GetSize(const Napi::CallbackInfo& info); | ||
| Napi::Value GetType(const Napi::CallbackInfo& info); | ||
| Napi::Value GetName(const Napi::CallbackInfo& info); | ||
| Napi::Value GetLastModified(const Napi::CallbackInfo& info); | ||
|
|
||
| Napi::Value ArrayBuffer(const Napi::CallbackInfo& info); | ||
| Napi::Value Text(const Napi::CallbackInfo& info); | ||
| Napi::Value Bytes(const Napi::CallbackInfo& info); | ||
|
|
||
| Napi::ObjectReference m_blob; | ||
| std::string m_name; | ||
| double m_lastModified{0.0}; | ||
| }; | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.