public void Run(string source) { JavaScriptModuleRecord root = JavaScriptModuleRecord.Initialize( null, rootSpecifier ); root.HostUrl = "<root>"; root.NotifyModuleReadyCallback = NotifyModuleReadyCallback; root.FetchImportedModuleCallBack = FetchImportedModuleCallback; try { root.ParseSource(source); } catch { // Exception gets handled by NotifyModuleReadyCallback } while (moduleParseQueue.Count > 0) { Loader.Debug("Parsing module from queue..."); moduleParseQueue.Dequeue() (); } if (executeRoot != null) { executeRoot(); executeRoot = null; } }
JavaScriptErrorCode FetchImportedModuleCallback( JavaScriptModuleRecord referencingModule, JavaScriptValue specifierAsValue, out JavaScriptModuleRecord outDependentModuleRecord ) { string specifier = specifierAsValue.ToString(); Loader.Debug($"Javascript.Module.RecordDelegate FetchImportedModuleCallback for specifier: '{specifier}'"); string normalizedSpecifier = resolver.Normalize(referencingModule.HostUrl, specifier); JavaScriptModuleRecord dependentModuleRecord; if (normalizedSpecifier == null) { dependentModuleRecord = JavaScriptModuleRecord.Initialize(null, "<invalid record>"); dependentModuleRecord.Exception = JavaScriptValue.CreateError($"Could not find module '{specifier}' from '{referencingModule.HostUrl}'"); } else if (cache.Has(normalizedSpecifier)) { dependentModuleRecord = cache.GetOrInvalid(normalizedSpecifier); } else { dependentModuleRecord = JavaScriptModuleRecord.Initialize(referencingModule, normalizedSpecifier); dependentModuleRecord.HostUrl = normalizedSpecifier; cache.Set(normalizedSpecifier, dependentModuleRecord); moduleParseQueue.Enqueue(() => { Loader.Debug($"Javascript.Module.RecordDelegate parsing source for '{specifier}'"); string source = ""; try { source = resolver.Read(normalizedSpecifier); } catch (Exception loadException) { dependentModuleRecord.Exception = JavaScriptValue.CreateError($"Couldn't read file '{normalizedSpecifier}': " + loadException); } try { // if we couldn't read the file for whatever reason, we must still call ParseSource // so that the internals will move on to the next step and show us the Exception set above dependentModuleRecord.ParseSource(source); // can throw syntax errors } catch { // will set Exception for us! } }); } dependencies.Add(dependentModuleRecord); outDependentModuleRecord = dependentModuleRecord; return(JavaScriptErrorCode.NoError); // Error case gets handled by Exception being set on the module record }
public string[] ValidateTemplate(string code) { var context = JavaScriptContext.CreateContext(_runtime); context.AddRef(); try { return(_dispatcher.Invoke(() => { using (context.GetScope()) { Native.ThrowIfError(Native.JsSetPromiseContinuationCallback(_promiseContinuationCallback, IntPtr.Zero)); // Load polyfill's JavaScriptContext.RunScript("const globalThis = this;"); JavaScriptContext.RunScript(PolyfillScripts.Get("ImageData")); try { var module = JavaScriptModuleRecord.Initialize(); module.FetchImportedModuleCallBack = (JavaScriptModuleRecord referencingModule, JavaScriptValue specifier, out JavaScriptModuleRecord dependentModuleRecord) => { dependentModuleRecord = JavaScriptModuleRecord.Invalid; return JavaScriptErrorCode.NoError; }; module.NotifyModuleReadyCallback = (JavaScriptModuleRecord referencingModule, JavaScriptValue exceptionVar) => JavaScriptErrorCode.NoError; module.ParseSource(code); return Array.Empty <string>(); } catch (JavaScriptException e) when(e.ErrorCode == JavaScriptErrorCode.ScriptCompile) { return new[] { e.Message }; } } })); } finally { context.Release(); } }
public void AddPreload(string specifier, string code) { if (!RootModule.IsValid) { throw new InvalidOperationException("No root module set"); } var module = JavaScriptModuleRecord.Initialize(RootModule, specifier); module.HostUrl = specifier; // Only for debugging _moduleLeases.Add(specifier, new ModuleLease(module)); _dispatcher.Invoke(() => { using (_context.GetScope()) { module.ParseSource(code); } }); }
/// <summary> /// User implemented callback to fetch additional imported modules in ES modules. /// </summary> /// <remarks> /// The callback is invoked on the current runtime execution thread, therefore execution is blocked until /// the callback completes. Notify the host to fetch the dependent module. This is the "import" part /// before HostResolveImportedModule in ES6 spec. This notifies the host that the referencing module has /// the specified module dependency, and the host needs to retrieve the module back. /// /// Callback should: /// 1. Check if the requested module has been requested before - if yes return the existing /// module record /// 2. If no create and initialize a new module record with JsInitializeModuleRecord to /// return and schedule a call to JsParseModuleSource for the new record. /// </remarks> /// <param name="referencingModule">The referencing module that is requesting the dependent module.</param> /// <param name="specifier">The specifier coming from the module source code.</param> /// <param name="dependentModuleRecord">The ModuleRecord of the dependent module. If the module was requested /// before from other source, return the existing ModuleRecord, otherwise /// return a newly created ModuleRecord.</param> /// <returns> /// Returns a <c>JsNoError</c> if the operation succeeded an error code otherwise. /// </returns> /// <see cref="JavaScriptFetchImportedModuleCallBack"/> private JavaScriptErrorCode LoadModuleImpl(JavaScriptModuleRecord referencingModule, JavaScriptValue specifier, out JavaScriptModuleRecord dependentModuleRecord) { var specifierString = specifier.ToString(); if (_moduleLeases.TryGetValue(specifierString, out var lease)) { dependentModuleRecord = lease.Module; return(JavaScriptErrorCode.NoError); } var alias = ResourceModuleUtils.GetResourceAlias(specifierString); if (alias is null) { dependentModuleRecord = JavaScriptModuleRecord.Initialize(referencingModule, specifier); dependentModuleRecord.HostUrl = specifierString; // Only for debugging dependentModuleRecord.Exception = JavaScriptValue.CreateTypeError($"Failed to resolve module for specifier '{specifierString}'"); _moduleLeases.Add(specifierString, new ModuleLease(dependentModuleRecord)); dependentModuleRecord = _moduleLeases[specifierString].Module; return(JavaScriptErrorCode.NoError); } dependentModuleRecord = JavaScriptModuleRecord.Initialize(referencingModule, specifier); dependentModuleRecord.HostUrl = specifierString; // Only for debugging _moduleLeases.Add(specifierString, new ModuleLease(dependentModuleRecord)); // Fire off a task in the threadpool Task.Run(async() => { var module = _moduleLeases[specifierString].Module; try { var resource = await _resourceManager.GetResourceAsync(alias); if (resource is object) { var script = _resourceScriptFactory.CreateFromExtension(resource, Path.GetExtension(specifierString)); _dispatcher.Invoke(() => { using (_context.GetScope()) { module.ParseSource(script); } }); } else { _dispatcher.Invoke(() => { using (_context.GetScope()) { ThrowModuleException(module, JavaScriptValue.CreateError($"Could not find the resource '{specifierString}'")); } }); } } catch (Exception e) { _dispatcher.Invoke(() => { using (_context.GetScope()) { ThrowModuleException(module, JavaScriptValue.CreateError(e.Message)); } }); } void ThrowModuleException(JavaScriptModuleRecord module, JavaScriptValue error) { error.AddRef(); module.Exception = error; if (!JavaScriptContext.HasException) { JavaScriptContext.SetException(error); } try { Native.ThrowIfError(JavaScriptErrorCode.ScriptException); } catch (Exception e) { OnResourceLoadError?.Invoke(e); } finally { error.Release(); } } }); return(JavaScriptErrorCode.NoError); }
public async Task <GenerateResult> GenerateDocumentAsync(string code, object model, IResourceManager resourceManager = null, CancellationToken cancellationToken = default) { // Multiple contexts can share a runtime, but only one thread can access the runtime at a time var context = JavaScriptContext.CreateContext(_runtime); context.AddRef(); ModuleLease rootModuleLease = null; try { using var loader = new CustomLoader(context, _dispatcher, _resourceScriptFactory, resourceManager); var tcs = new TaskCompletionSource <JavaScriptValue>(TaskCreationOptions.RunContinuationsAsynchronously); _dispatcher.Invoke(() => { using (context.GetScope()) { Native.ThrowIfError(Native.JsSetPromiseContinuationCallback(_promiseContinuationCallback, IntPtr.Zero)); // Load polyfill's JavaScriptContext.RunScript("const globalThis = this;"); JavaScriptContext.RunScript(PolyfillScripts.Get("ImageData")); var rootModule = JavaScriptModuleRecord.Initialize(); rootModule.AddRef(); rootModule.HostUrl = "<host>"; // Only for debugging loader.RootModule = rootModule; rootModule.FetchImportedModuleCallBack = loader.LoadModule; rootModule.NotifyModuleReadyCallback = loader.ModuleLoaded; rootModuleLease = new ModuleLease(rootModule); loader.AddPreload("main", code); } }); // Add callback that will be run when the root module has been evaluated, // at which time its rootModule.Namespace becomes valid. loader.OnRootModuleEvaluated = () => { using (context.GetScope()) { var build = rootModuleLease.Module.Namespace.GetProperty("build"); build.AddRef(); var modelJson = JavaScriptValue.FromString(JsonSerializer.Serialize(model)); modelJson.AddRef(); var resultPromise = build.CallFunction(JavaScriptValue.GlobalObject, modelJson); resultPromise.AddRef(); FunctionLease resolve = default; FunctionLease reject = default; FunctionLease always = default; resolve = new FunctionLease((callee, isConstructCall, arguments, argumentCount, callbackState) => { var result = arguments[1]; result.AddRef(); tcs.SetResult(result); return(callee); }); reject = new FunctionLease((callee, isConstructCall, arguments, argumentCount, callbackState) => { JavaScriptException exception; if (arguments.Length >= 2) { var reason = arguments[1]; if (reason.ValueType == JavaScriptValueType.Error) { exception = new JavaScriptException(JavaScriptErrorCode.ScriptException, reason.GetProperty("message").ToString()); } else { exception = new JavaScriptException(JavaScriptErrorCode.ScriptException, reason.ConvertToString().ToString()); } } else { exception = new JavaScriptException(JavaScriptErrorCode.ScriptException); } tcs.SetException(exception); return(callee); }); always = new FunctionLease((callee, isConstructCall, arguments, argumentCount, callbackState) => { build.Release(); modelJson.Release(); resultPromise.Release(); resolve.Dispose(); reject.Dispose(); always.Dispose(); return(callee); }); resultPromise .GetProperty("then").CallFunction(resultPromise, resolve.Function) .GetProperty("catch").CallFunction(resultPromise, reject.Function) .GetProperty("finally").CallFunction(resultPromise, always.Function); } }; loader.OnResourceLoadError = e => tcs.SetException(e); _dispatcher.Invoke(() => { using (context.GetScope()) { rootModuleLease.Module.ParseSource(@" import Builder from 'main'; const builder = new Builder(); const build = async (modelJson) => { const model = JSON.parse(modelJson); const content = await Promise.resolve(builder.build(model)); try { const { contentType } = await import('main'); return { content, contentType }; } catch (error) { return { content }; } }; export { build }; "); } }); var result = await tcs.Task; return(_dispatcher.Invoke(() => { using (context.GetScope()) { _runtime.CollectGarbage(); var content = result.GetProperty("content"); var contentTypeValue = result.GetProperty("contentType"); var contentType = contentTypeValue.ValueType == JavaScriptValueType.String ? contentTypeValue.ToString() : null; switch (content.ValueType) { case JavaScriptValueType.String: return new GenerateResult() { Content = Encoding.UTF8.GetBytes(content.ToString()), ContentType = contentType ?? "text/plain" }; case JavaScriptValueType.TypedArray: { content.GetTypedArrayInfo(out var arrayType, out var buffer, out var byteOffset, out var byteLength); if (arrayType != JavaScriptTypedArrayType.Uint8) { throw new NotSupportedException("Typed array must be Uint8Array"); } var bufferPointer = buffer.GetArrayBufferStorage(out var bufferLength); var array = new byte[byteLength]; Marshal.Copy(IntPtr.Add(bufferPointer, (int)byteOffset), array, 0, (int)byteLength); return new GenerateResult() { Content = array, ContentType = contentType ?? "application/octet-stream" }; } case JavaScriptValueType.Array: { var list = content.ToList(); var array = new byte[list.Count]; for (var i = 0; i < list.Count; i++) { array[i] = (byte)list[i].ToInt32(); } return new GenerateResult() { Content = array, ContentType = contentType ?? "application/octet-stream" }; } default: throw new NotSupportedException("Build did not produce a supported result"); } } })); } finally { _dispatcher.Invoke(() => { using (context.GetScope()) { rootModuleLease?.Dispose(); } }); context.Release(); } }