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 async Task <GenerateResult> GenerateDocumentAsync(string code, object model, IResourceManager resourceManager = null, CancellationToken cancellationToken = default) { using var engine = new V8ScriptEngine(V8ScriptEngineFlags.EnableDynamicModuleImports); engine.Execute(PolyfillScripts.Get("ImageData")); engine.DocumentSettings.Loader = new CustomLoader(engine.DocumentSettings.Loader, _resourceScriptFactory, resourceManager); engine.DocumentSettings.AddSystemDocument("main", ModuleCategory.Standard, code); dynamic setModel = engine.Evaluate(@" let model; const setModel = m => model = JSON.parse(m); setModel"); setModel(JsonSerializer.Serialize(model)); dynamic contentTypePromise = engine.Evaluate(new DocumentInfo() { Category = ModuleCategory.Standard }, @" async function getContentType() { const { contentType } = await import('main'); return contentType; } getContentType()"); var contentType = await ToTask(contentTypePromise); if (contentType is Undefined) { contentType = null; } dynamic resultPromise = engine.Evaluate(new DocumentInfo() { Category = ModuleCategory.Standard }, @" import Builder from 'main'; let builder = new Builder(); Promise.resolve(builder.build(model));"); var result = await ToTask(resultPromise); switch (result) { case string @string: return(new GenerateResult() { Content = Encoding.UTF8.GetBytes(@string), ContentType = contentType ?? "text/plain" }); case ITypedArray <byte> typedArray: return(new GenerateResult() { Content = typedArray.ToArray(), ContentType = contentType ?? "application/octet-stream" }); case IList list: { var array = new byte[list.Count]; for (var i = 0; i < list.Count; i++) { array[i] = Convert.ToByte(list[i]); } return(new GenerateResult() { Content = array, ContentType = contentType ?? "application/octet-stream" }); } default: throw new NotSupportedException("Build did not produce a supported result"); } }
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(); } }