From fed7865e27bfbb24395ee493fba02b4ab05c256a Mon Sep 17 00:00:00 2001 From: Marko Lahma Date: Fri, 6 Mar 2026 16:59:13 +0200 Subject: [PATCH] Implement import-bytes module support Add support for the TC39 import-bytes proposal which enables importing binary files as immutable Uint8Array via import attributes: `import data from './file.bin' with { type: 'bytes' }` Extends the module loading pipeline with LoadModuleContentsAsBytes() virtual method and BuildBytesModule() factory for creating synthetic modules backed by immutable ArrayBuffers. Co-Authored-By: Claude Opus 4.6 --- .../Test262Harness.settings.json | 1 - Jint.Tests.Test262/Test262ModuleLoader.cs | 16 ++++++++++++ Jint/Runtime/Modules/DefaultModuleLoader.cs | 22 ++++++++++++++++ Jint/Runtime/Modules/ModuleFactory.cs | 18 +++++++++++++ Jint/Runtime/Modules/ModuleLoader.cs | 25 +++++++++++++++++++ .../Modules/ModuleRequestExtensions.cs | 17 +++++++++++++ README.md | 2 ++ 7 files changed, 100 insertions(+), 1 deletion(-) diff --git a/Jint.Tests.Test262/Test262Harness.settings.json b/Jint.Tests.Test262/Test262Harness.settings.json index 378084e038..e5d7088735 100644 --- a/Jint.Tests.Test262/Test262Harness.settings.json +++ b/Jint.Tests.Test262/Test262Harness.settings.json @@ -5,7 +5,6 @@ "Namespace": "Jint.Tests.Test262", "Parallel": true, "ExcludedFeatures": [ - "import-bytes", "import-defer", "regexp-lookbehind", "regexp-modifiers", diff --git a/Jint.Tests.Test262/Test262ModuleLoader.cs b/Jint.Tests.Test262/Test262ModuleLoader.cs index 5d36e17253..0af891c7e9 100644 --- a/Jint.Tests.Test262/Test262ModuleLoader.cs +++ b/Jint.Tests.Test262/Test262ModuleLoader.cs @@ -35,4 +35,20 @@ protected override string LoadModuleContents(Engine engine, ResolvedSpecifier re return stream.ReadToEnd(); } } + + protected override byte[] LoadModuleContentsAsBytes(Engine engine, ResolvedSpecifier resolved) + { + lock (_fileSystem) + { + var fileName = Path.Combine(_basePath, resolved.Key).Replace('\\', '/'); + if (!_fileSystem.FileExists(fileName)) + { + Throw.ModuleResolutionException("Module Not Found", resolved.ModuleRequest.Specifier, parent: null, fileName); + } + using var stream = _fileSystem.OpenFile(fileName, FileMode.Open, FileAccess.Read); + using var ms = new MemoryStream(); + stream.CopyTo(ms); + return ms.ToArray(); + } + } } diff --git a/Jint/Runtime/Modules/DefaultModuleLoader.cs b/Jint/Runtime/Modules/DefaultModuleLoader.cs index 4279009cb6..71385f8d04 100644 --- a/Jint/Runtime/Modules/DefaultModuleLoader.cs +++ b/Jint/Runtime/Modules/DefaultModuleLoader.cs @@ -143,6 +143,28 @@ protected override string LoadModuleContents(Engine engine, ResolvedSpecifier re return File.ReadAllText(fileName); } + protected override byte[] LoadModuleContentsAsBytes(Engine engine, ResolvedSpecifier resolved) + { + var specifier = resolved.ModuleRequest.Specifier; + if (resolved.Type != SpecifierType.RelativeOrAbsolute) + { + Throw.NotSupportedException($"The default module loader can only resolve files. You can define modules directly to allow imports using {nameof(Engine)}.{nameof(Engine.Modules.Add)}(). Attempted to resolve: '{specifier}'."); + } + + if (resolved.Uri == null) + { + Throw.InvalidOperationException($"Module '{specifier}' of type '{resolved.Type}' has no resolved URI."); + } + + var fileName = Uri.UnescapeDataString(resolved.Uri.AbsolutePath); + if (!File.Exists(fileName)) + { + Throw.ModuleResolutionException("Module Not Found", specifier, parent: null, fileName); + } + + return File.ReadAllBytes(fileName); + } + private static bool IsRelative(string specifier) { return specifier.StartsWith('.') || specifier.StartsWith('/'); diff --git a/Jint/Runtime/Modules/ModuleFactory.cs b/Jint/Runtime/Modules/ModuleFactory.cs index 35de4b6eb7..59f1338811 100644 --- a/Jint/Runtime/Modules/ModuleFactory.cs +++ b/Jint/Runtime/Modules/ModuleFactory.cs @@ -1,4 +1,5 @@ using Jint.Native; +using Jint.Native.ArrayBuffer; using Jint.Native.Json; namespace Jint.Runtime.Modules; @@ -87,4 +88,21 @@ public static Module BuildJsonModule(Engine engine, JsValue parsedJson, string? { return new SyntheticModule(engine, engine.Realm, parsedJson, location); } + + /// + /// Creates a for the usage within the given for the + /// provided bytes module data. + /// + /// + /// https://tc39.es/proposal-import-bytes/#sec-create-bytes-module + /// + public static Module BuildBytesModule(Engine engine, ResolvedSpecifier resolved, byte[] bytes) + { + var arrayBuffer = engine.Realm.Intrinsics.ArrayBuffer.Construct(bytes); + arrayBuffer._isImmutable = true; + + var uint8Array = engine.Realm.Intrinsics.Uint8Array.Construct([arrayBuffer], engine.Realm.Intrinsics.Uint8Array); + + return new SyntheticModule(engine, engine.Realm, uint8Array, resolved.Uri?.LocalPath); + } } diff --git a/Jint/Runtime/Modules/ModuleLoader.cs b/Jint/Runtime/Modules/ModuleLoader.cs index 6b0a891ac7..d244703433 100644 --- a/Jint/Runtime/Modules/ModuleLoader.cs +++ b/Jint/Runtime/Modules/ModuleLoader.cs @@ -9,6 +9,22 @@ public abstract class ModuleLoader : IModuleLoader public Module LoadModule(Engine engine, ResolvedSpecifier resolved) { + if (resolved.ModuleRequest.IsBytesModule()) + { + byte[] bytes; + try + { + bytes = LoadModuleContentsAsBytes(engine, resolved); + } + catch (Exception) + { + Throw.JavaScriptException(engine, $"Could not load module {resolved.ModuleRequest.Specifier}", AstExtensions.DefaultLocation); + return default!; + } + + return ModuleFactory.BuildBytesModule(engine, resolved, bytes); + } + string code; try { @@ -29,4 +45,13 @@ public Module LoadModule(Engine engine, ResolvedSpecifier resolved) } protected abstract string LoadModuleContents(Engine engine, ResolvedSpecifier resolved); + + /// + /// Loads module contents as raw bytes. Override in derived classes for efficient binary loading. + /// + protected virtual byte[] LoadModuleContentsAsBytes(Engine engine, ResolvedSpecifier resolved) + { + var text = LoadModuleContents(engine, resolved); + return System.Text.Encoding.UTF8.GetBytes(text); + } } diff --git a/Jint/Runtime/Modules/ModuleRequestExtensions.cs b/Jint/Runtime/Modules/ModuleRequestExtensions.cs index ee4da9a468..06d592845b 100644 --- a/Jint/Runtime/Modules/ModuleRequestExtensions.cs +++ b/Jint/Runtime/Modules/ModuleRequestExtensions.cs @@ -18,4 +18,21 @@ public static bool IsJsonModule(this ModuleRequest request) return request.Attributes != null && Array.Exists(request.Attributes, x => string.Equals(x.Key, "type", StringComparison.Ordinal) && string.Equals(x.Value, "json", StringComparison.Ordinal)); } + + /// + /// Returns true if the provided + /// is a bytes module, otherwise false. + /// + /// + /// The following JavaScript import statement imports a bytes module + /// for which this method would return true. + /// + /// import value from 'data.bin' with { type: 'bytes' } + /// + /// + public static bool IsBytesModule(this ModuleRequest request) + { + return request.Attributes != null + && Array.Exists(request.Attributes, x => string.Equals(x.Key, "type", StringComparison.Ordinal) && string.Equals(x.Value, "bytes", StringComparison.Ordinal)); + } } diff --git a/README.md b/README.md index 780844acfd..7fe19a070f 100644 --- a/README.md +++ b/README.md @@ -144,9 +144,11 @@ and many more. #### ECMAScript proposals (no version yet) +- ✔ Await Dictionary (`Promise.allKeyed`, `Promise.allSettledKeyed`) - ✔ `Error.isError` - ✔ Explicit Resource Management (`using` and `await using`) - ✔ Immutable Arraybuffers +- ✔ Import Bytes (`import x from './file' with { type: 'bytes' }`) - ✔ Iterator Sequencing - ✔ Joint Iteration - ✔ JSON.parse source text access