From ff6c1883ad55898cf592b7a448b0b8163b4c8adb Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 15 Nov 2024 22:42:53 +0300 Subject: [PATCH 01/16] bump dot net version --- samples/minimal/cs/Minimal.csproj | 2 +- .../react/backend/Backend.Prime/Backend.Prime.csproj | 2 +- .../react/backend/Backend.WASM/Backend.WASM.csproj | 4 ++-- samples/react/backend/Backend/Backend.csproj | 2 +- samples/trimming/README.md | 1 + samples/trimming/cs/Trimming.csproj | 2 +- .../Bootsharp.Common.Test.csproj | 4 ++-- src/cs/Bootsharp.Common/Bootsharp.Common.csproj | 2 +- .../Bootsharp.Generate.Test.csproj | 4 ++-- .../Bootsharp.Inject.Test.csproj | 6 +++--- src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj | 4 ++-- .../Bootsharp.Publish.Test.csproj | 8 ++++---- src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj | 8 ++++---- src/cs/Bootsharp/Bootsharp.csproj | 2 +- src/js/package.json | 12 ++++++------ src/js/test/cs/Test.Types/Test.Types.csproj | 2 +- src/js/test/cs/Test/Test.csproj | 4 ++-- 17 files changed, 35 insertions(+), 34 deletions(-) diff --git a/samples/minimal/cs/Minimal.csproj b/samples/minimal/cs/Minimal.csproj index 3d546b12..6a0a54f1 100644 --- a/samples/minimal/cs/Minimal.csproj +++ b/samples/minimal/cs/Minimal.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 browser-wasm diff --git a/samples/react/backend/Backend.Prime/Backend.Prime.csproj b/samples/react/backend/Backend.Prime/Backend.Prime.csproj index db766c7d..d3fb8b97 100644 --- a/samples/react/backend/Backend.Prime/Backend.Prime.csproj +++ b/samples/react/backend/Backend.Prime/Backend.Prime.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable diff --git a/samples/react/backend/Backend.WASM/Backend.WASM.csproj b/samples/react/backend/Backend.WASM/Backend.WASM.csproj index 646cb936..05d5c007 100644 --- a/samples/react/backend/Backend.WASM/Backend.WASM.csproj +++ b/samples/react/backend/Backend.WASM/Backend.WASM.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 browser-wasm enable @@ -23,7 +23,7 @@ - + diff --git a/samples/react/backend/Backend/Backend.csproj b/samples/react/backend/Backend/Backend.csproj index d36f76db..e57a2314 100644 --- a/samples/react/backend/Backend/Backend.csproj +++ b/samples/react/backend/Backend/Backend.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 diff --git a/samples/trimming/README.md b/samples/trimming/README.md index 64c1ac70..b39009e9 100644 --- a/samples/trimming/README.md +++ b/samples/trimming/README.md @@ -9,3 +9,4 @@ To test and measure build size: | .NET | Raw | Brotli | |-------|-------|--------| | 8.0.1 | 2,298 | 739 | +| 9.0.1 | 2,369 | 761 | diff --git a/samples/trimming/cs/Trimming.csproj b/samples/trimming/cs/Trimming.csproj index 5fb20a19..6858f874 100644 --- a/samples/trimming/cs/Trimming.csproj +++ b/samples/trimming/cs/Trimming.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 browser-wasm false diff --git a/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj b/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj index 2247ac5e..21f91c1b 100644 --- a/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj +++ b/src/cs/Bootsharp.Common.Test/Bootsharp.Common.Test.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable false @@ -12,7 +12,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/cs/Bootsharp.Common/Bootsharp.Common.csproj b/src/cs/Bootsharp.Common/Bootsharp.Common.csproj index b52f6d7a..1370ca83 100644 --- a/src/cs/Bootsharp.Common/Bootsharp.Common.csproj +++ b/src/cs/Bootsharp.Common/Bootsharp.Common.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable Bootsharp.Common diff --git a/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj b/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj index 0a9e3599..06522d3b 100644 --- a/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj +++ b/src/cs/Bootsharp.Generate.Test/Bootsharp.Generate.Test.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable false @@ -14,7 +14,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj b/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj index d98d0fca..65172a1c 100644 --- a/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj +++ b/src/cs/Bootsharp.Inject.Test/Bootsharp.Inject.Test.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable false true @@ -12,9 +12,9 @@ - + - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj b/src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj index adac40d0..0362d1b4 100644 --- a/src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj +++ b/src/cs/Bootsharp.Inject/Bootsharp.Inject.csproj @@ -1,7 +1,7 @@  - net8.0 + net9.0 enable enable Bootsharp.Inject @@ -13,7 +13,7 @@ - + diff --git a/src/cs/Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj b/src/cs/Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj index 8e74ee05..08da01e6 100644 --- a/src/cs/Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj +++ b/src/cs/Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable false @@ -12,10 +12,10 @@ - + - - + + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj b/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj index 854b8fb6..191e91ef 100644 --- a/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj +++ b/src/cs/Bootsharp.Publish/Bootsharp.Publish.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable false @@ -9,9 +9,9 @@ - - - + + + diff --git a/src/cs/Bootsharp/Bootsharp.csproj b/src/cs/Bootsharp/Bootsharp.csproj index 27daffb0..bb3ee012 100644 --- a/src/cs/Bootsharp/Bootsharp.csproj +++ b/src/cs/Bootsharp/Bootsharp.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable enable Bootsharp diff --git a/src/js/package.json b/src/js/package.json index 44ec6e62..8cb797c5 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -6,11 +6,11 @@ "build": "sh scripts/build.sh" }, "devDependencies": { - "typescript": "^5.4.5", - "@types/node": "^20.12.12", - "@types/ws": "^8.5.10", - "vitest": "^1.6.0", - "@vitest/coverage-v8": "^1.6.0", - "ws": "^8.17.0" + "typescript": "5.6.3", + "@types/node": "22.9.0", + "@types/ws": "8.5.13", + "vitest": "2.1.5", + "@vitest/coverage-v8": "2.1.5", + "ws": "8.18.0" } } diff --git a/src/js/test/cs/Test.Types/Test.Types.csproj b/src/js/test/cs/Test.Types/Test.Types.csproj index 3f2f63dc..5a5bfd8d 100644 --- a/src/js/test/cs/Test.Types/Test.Types.csproj +++ b/src/js/test/cs/Test.Types/Test.Types.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 true bin/codegen diff --git a/src/js/test/cs/Test/Test.csproj b/src/js/test/cs/Test/Test.csproj index 4777c465..9b0e116a 100644 --- a/src/js/test/cs/Test/Test.csproj +++ b/src/js/test/cs/Test/Test.csproj @@ -1,7 +1,7 @@ - net8.0 + net9.0 enable browser-wasm true @@ -16,7 +16,7 @@ - + From 19f46e60d617cfcef6f8559e95627693242d8f1d Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 15 Nov 2024 22:43:10 +0300 Subject: [PATCH 02/16] fix target --- src/cs/Bootsharp/Build/Bootsharp.targets | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index b2561cb8..08c2932a 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -84,7 +84,7 @@ - @@ -105,8 +105,6 @@ - Date: Fri, 22 Nov 2024 18:22:57 +0300 Subject: [PATCH 03/16] temp ver --- src/cs/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 179b46e4..0a2eda72 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.3.3 + 0.4.0-alpha.3 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com From 8dfa509e4aa8c05156e35fbe2cf99deed1ddc8ed Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:28:39 +0300 Subject: [PATCH 04/16] remove unused tests --- src/cs/Bootsharp.Common.Test/InterfacesTest.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/cs/Bootsharp.Common.Test/InterfacesTest.cs b/src/cs/Bootsharp.Common.Test/InterfacesTest.cs index 58fc049b..89a32085 100644 --- a/src/cs/Bootsharp.Common.Test/InterfacesTest.cs +++ b/src/cs/Bootsharp.Common.Test/InterfacesTest.cs @@ -2,18 +2,10 @@ public class InterfacesTest { - [Fact] - public void Records () - { - // TODO: Remove once coverlet properly handles record coverage. - _ = new ExportInterface(default, default) with { Interface = typeof(int) }; - _ = new ImportInterface(default) with { Instance = "" }; - } - [Fact] public void RegistersExports () { - var export = new ExportInterface(typeof(IBackend), default); + var export = new ExportInterface(typeof(IBackend), null); Interfaces.Register(typeof(Backend), export); Assert.Equal(typeof(IBackend), Interfaces.Exports[typeof(Backend)].Interface); } From 0029a192d92247f25afed131e3e9715f97a2fd90 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 15 Dec 2024 18:28:49 +0300 Subject: [PATCH 05/16] fix cover script --- src/cs/.scripts/cover.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs/.scripts/cover.ps1 b/src/cs/.scripts/cover.ps1 index 5fe991b2..4834126d 100644 --- a/src/cs/.scripts/cover.ps1 +++ b/src/cs/.scripts/cover.ps1 @@ -7,7 +7,7 @@ try { dotnet test Bootsharp.Publish.Test/Bootsharp.Publish.Test.csproj /p:CollectCoverage=true /p:CoverletOutputFormat="json%2copencover" /p:ExcludeByAttribute=GeneratedCodeAttribute /p:CoverletOutput=$out /p:MergeWith=$json reportgenerator "-reports:*/*.xml" "-targetdir:.cover" -reporttypes:HTML python -m webbrowser http://localhost:3000 - serve .cover + npx serve .cover } finally { rm .cover -r -force } From 4e7f5410f538323cbf4e6d64383b321f9dea34b7 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Mon, 23 Dec 2024 14:01:59 +0300 Subject: [PATCH 06/16] iteration (todo: test changes) --- samples/trimming/main.mjs | 10 +- src/cs/Bootsharp/Build/Bootsharp.targets | 2 +- src/cs/Directory.Build.props | 2 +- src/js/package.json | 8 +- src/js/scripts/compile-test.sh | 4 +- src/js/src/boot.ts | 4 +- src/js/src/config.ts | 13 +- src/js/src/decoder.ts | 61 +++-- src/js/src/dotnet.g.d.ts | 316 +++++++++++++++++++---- src/js/test/cs/Test/Test.csproj | 1 - src/js/test/spec/boot.spec.ts | 42 +-- 11 files changed, 354 insertions(+), 109 deletions(-) diff --git a/samples/trimming/main.mjs b/samples/trimming/main.mjs index 8cf4c2b7..a4bed2d4 100644 --- a/samples/trimming/main.mjs +++ b/samples/trimming/main.mjs @@ -1,7 +1,9 @@ import bootsharp, { Program } from "./cs/bin/bootsharp/index.mjs"; +import { pathToFileURL } from "node:url"; +import fs from "node:fs/promises"; import zlib from "node:zlib"; import util from "node:util"; -import fs from "node:fs/promises"; +import path from "node:path"; console.log(`Binary size: ${await measure("./cs/bin/bootsharp/bin")}KB`); console.log(`Brotli size: ${await measure("./cs/bin/bootsharp/bro")}KB`); @@ -13,13 +15,13 @@ await Promise.all([ ]); Program.log = console.log; -await bootsharp.boot({ root: "./bin", resources }); +const root = pathToFileURL(path.resolve("./cs/bin/bootsharp/bin")); +await bootsharp.boot({ root, resources }); async function measure(dir) { let size = 0; - for await (const entry of await fs.opendir(dir)) { + for await (const entry of await fs.opendir(dir)) size += (await fs.stat(`${entry.path}/${entry.name}`)).size; - } return Math.ceil(size / 1024); } diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 08c2932a..8170da11 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -84,7 +84,7 @@ - diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 0a2eda72..986603f7 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.4.0-alpha.3 + 0.4.0-alpha.22 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com diff --git a/src/js/package.json b/src/js/package.json index 8cb797c5..3d1794ab 100644 --- a/src/js/package.json +++ b/src/js/package.json @@ -6,11 +6,11 @@ "build": "sh scripts/build.sh" }, "devDependencies": { - "typescript": "5.6.3", - "@types/node": "22.9.0", + "typescript": "5.7.2", + "@types/node": "22.10.2", "@types/ws": "8.5.13", - "vitest": "2.1.5", - "@vitest/coverage-v8": "2.1.5", + "vitest": "2.1.8", + "@vitest/coverage-v8": "2.1.8", "ws": "8.18.0" } } diff --git a/src/js/scripts/compile-test.sh b/src/js/scripts/compile-test.sh index 2ef778d9..4f92438a 100644 --- a/src/js/scripts/compile-test.sh +++ b/src/js/scripts/compile-test.sh @@ -1,3 +1,3 @@ cd test/cs -dotnet publish -p:BootsharpName=embedded -p:BootsharpEmbedBinaries=true -dotnet publish -p:BootsharpName=sideload -p:BootsharpEmbedBinaries=false +dotnet publish -p BootsharpName=embedded -p BootsharpEmbedBinaries=true -p RunAOTCompilation=true +dotnet publish -p BootsharpName=sideload -p BootsharpEmbedBinaries=false -p RunAOTCompilation=true diff --git a/src/js/src/boot.ts b/src/js/src/boot.ts index 1d9405e8..d363f628 100644 --- a/src/js/src/boot.ts +++ b/src/js/src/boot.ts @@ -16,7 +16,7 @@ export enum BootStatus { /** Boot process configuration. */ export type BootOptions = { - /** Path to directory where boot resources are hosted (eg, /bin). */ + /** Absolute path to the directory where boot resources are hosted (eg, /bin). */ readonly root?: string; /** Resources required to boot .NET runtime. */ readonly resources?: BootResources; @@ -50,8 +50,6 @@ export async function boot(options?: BootOptions): Promise { main = await getMain(options?.root); const config = options?.config ?? await buildConfig(options?.resources ?? resources, options?.root); const runtime = await options?.create?.(config) || await main.dotnet.withConfig(config).create(); - // TODO: Remove once https://github.com/dotnet/runtime/issues/92713 fix is merged. - (<{ runtimeKeepalivePush: () => void }>runtime.Module).runtimeKeepalivePush(); await options?.import?.(runtime) || bindImports(runtime); await options?.run?.(runtime) || await runtime.runMain(config.mainAssemblyName!, []); await options?.export?.(runtime) || await bindExports(runtime, config.mainAssemblyName!); diff --git a/src/js/src/config.ts b/src/js/src/config.ts index 2b67ba98..9c327805 100644 --- a/src/js/src/config.ts +++ b/src/js/src/config.ts @@ -10,7 +10,7 @@ export async function buildConfig(resources: BootResources, root?: string): Prom const main = embed ? await getMain() : undefined; const native = embed ? await getNative() : undefined; const runtime = embed ? await getRuntime() : undefined; - const mt = !embed && (await import("./dotnet.g")).mt; + // const mt = !embed && (await import("./dotnet.g")).mt; return { mainAssemblyName: resources.entryAssemblyName, assets: [ @@ -25,15 +25,10 @@ export async function buildConfig(resources: BootResources, root?: string): Prom function buildAsset(res: BinaryResource, behavior: AssetBehaviors, module?: unknown, optional?: boolean): AssetEntry { - const url = `${root}/${res.name}`; return { - // Due to dotnet bug resolvedUrl is not transferred to worker before the runtime - // is initialized, hence we're assigning URL to the name for the JS and WASM modules - // (assemblies are not affected). This is only relevant for multithreading mode. - // TODO: Revise after dotnet fix https://github.com/dotnet/runtime/issues/93133. - name: (!mt || res.content || behavior === "assembly") ? res.name : url, - resolvedUrl: (res.content || !root) ? undefined : url, - buffer: typeof res.content === "string" ? decodeBase64(res.content) : res.content, + name: res.name, + resolvedUrl: (res.content || !root) ? undefined : `${root}/${res.name}`, + buffer: typeof res.content === "string" ? decodeBase64(res.content) : res.content?.buffer, moduleExports: module, isOptional: optional, behavior diff --git a/src/js/src/decoder.ts b/src/js/src/decoder.ts index 09111f18..93d2e9f2 100644 --- a/src/js/src/decoder.ts +++ b/src/js/src/decoder.ts @@ -1,35 +1,56 @@ const lookup = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 62, 0, 62, 0, 63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 0, 0, 0, 0, 63, 0, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51]); -export function decodeBase64(source: string): Uint8Array { - if (typeof window === "object") return Uint8Array.from(window.atob(source), c => c.charCodeAt(0)); - if (typeof Buffer === "function") return Buffer.from(source, "base64"); +export function decodeBase64(source: string): ArrayBuffer { + if (typeof window === "object") return decodeWithBrowser(source); + if (typeof Buffer === "function") return decodeWithNodeJS(source); + return decodeNaive(source); +} + +function decodeWithBrowser(source: string): ArrayBuffer { + const binaryString = window.atob(source); + const length = binaryString.length; + const buffer = new ArrayBuffer(length); + const uint8Array = new Uint8Array(buffer); + for (let i = 0; i < length; i++) + uint8Array[i] = binaryString.charCodeAt(i); + return buffer; +} - const sourceLength = source.length; - const paddingLength = (source[sourceLength - 2] === "=" ? 2 : (source[sourceLength - 1] === "=" ? 1 : 0)); - const baseLength = (sourceLength - paddingLength) & 0xfffffffc; +function decodeWithNodeJS(source: string): ArrayBuffer { + const buffer = Buffer.from(source, "base64"); + return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); +} + +function decodeNaive(source: string): ArrayBuffer { + const srcLen = source.length; + const padLen = (source[srcLen - 2] === "=" ? 2 : (source[srcLen - 1] === "=" ? 1 : 0)); + const outLen = ((srcLen - padLen) * 3) >> 2; + const buffer = new Uint8Array(outLen); let tmp; - let i = 0; let byteIndex = 0; - const buffer = []; - for (; i < baseLength; i += 4) { - tmp = (lookup[source.charCodeAt(i)] << 18) | (lookup[source.charCodeAt(i + 1)] << 12) | (lookup[source.charCodeAt(i + 2)] << 6) | (lookup[source.charCodeAt(i + 3)]); + for (let i = 0, baseLen = srcLen - padLen; i < baseLen; i += 4) { + tmp = (lookup[source.charCodeAt(i)] << 18) + | (lookup[source.charCodeAt(i + 1)] << 12) + | (lookup[source.charCodeAt(i + 2)] << 6) + | (lookup[source.charCodeAt(i + 3)]); buffer[byteIndex++] = (tmp >> 16) & 0xFF; buffer[byteIndex++] = (tmp >> 8) & 0xFF; - buffer[byteIndex++] = (tmp) & 0xFF; - } - - if (paddingLength === 1) { - tmp = (lookup[source.charCodeAt(i)] << 10) | (lookup[source.charCodeAt(i + 1)] << 4) | (lookup[source.charCodeAt(i + 2)] >> 2); - buffer[byteIndex++] = (tmp >> 8) & 0xFF; buffer[byteIndex++] = tmp & 0xFF; } - if (paddingLength === 2) { - tmp = (lookup[source.charCodeAt(i)] << 2) | (lookup[source.charCodeAt(i + 1)] >> 4); - buffer[byteIndex++] = tmp & 0xFF; + if (padLen === 1) { + tmp = (lookup[source.charCodeAt(srcLen - 4)] << 18) + | (lookup[source.charCodeAt(srcLen - 3)] << 12) + | (lookup[source.charCodeAt(srcLen - 2)] << 6); + buffer[byteIndex++] = (tmp >> 16) & 0xFF; + buffer[byteIndex++] = (tmp >> 8) & 0xFF; + } else if (padLen === 2) { + tmp = (lookup[source.charCodeAt(srcLen - 4)] << 18) + | (lookup[source.charCodeAt(srcLen - 3)] << 12); + buffer[byteIndex++] = (tmp >> 16) & 0xFF; } - return new Uint8Array(buffer); + return buffer.buffer; } diff --git a/src/js/src/dotnet.g.d.ts b/src/js/src/dotnet.g.d.ts index 29771c9d..3badad82 100644 --- a/src/js/src/dotnet.g.d.ts +++ b/src/js/src/dotnet.g.d.ts @@ -2,7 +2,7 @@ export const embedded = false; export const mt = false; -// Types: https://github.com/dotnet/runtime/blob/main/src/mono/wasm/runtime/dotnet.d.ts +// Types: https://github.com/dotnet/runtime/blob/v9.0.0/src/mono/browser/runtime/dotnet.d.ts declare interface NativePointer { __brandNativePointer: "NativePointer"; @@ -17,26 +17,9 @@ declare interface Int32Ptr extends NativePointer { __brand: "Int32Ptr"; } declare interface EmscriptenModule { - /** @deprecated Please use growableHeapI8() instead.*/ - HEAP8: Int8Array; - /** @deprecated Please use growableHeapI16() instead.*/ - HEAP16: Int16Array; - /** @deprecated Please use growableHeapI32() instead. */ - HEAP32: Int32Array; - /** @deprecated Please use growableHeapI64() instead. */ - HEAP64: BigInt64Array; - /** @deprecated Please use growableHeapU8() instead. */ - HEAPU8: Uint8Array; - /** @deprecated Please use growableHeapU16() instead. */ - HEAPU16: Uint16Array; - /** @deprecated Please use growableHeapU32() instead */ - HEAPU32: Uint32Array; - /** @deprecated Please use growableHeapF32() instead */ - HEAPF32: Float32Array; - /** @deprecated Please use growableHeapF64() instead. */ - HEAPF64: Float64Array; _malloc(size: number): VoidPtr; _free(ptr: VoidPtr): void; + _sbrk(size: number): VoidPtr; out(message: string): void; err(message: string): void; ccall(ident: string, returnType?: string | null, argTypes?: string[], args?: any[], opts?: any): T; @@ -48,6 +31,7 @@ declare interface EmscriptenModule { UTF8ToString(ptr: CharPtr, maxBytesToRead?: number): string; UTF8ArrayToString(u8Array: Uint8Array, idx?: number, maxBytesToRead?: number): string; stringToUTF8Array(str: string, heap: Uint8Array, outIdx: number, maxBytesToWrite: number): void; + lengthBytesUTF8(str: string): number; FS_createPath(parent: string, path: string, canRead?: boolean, canWrite?: boolean): string; FS_createDataFile(parent: string, name: string, data: TypedArray, canRead: boolean, canWrite: boolean, canOwn?: boolean): string; addFunction(fn: Function, signature: string): number; @@ -71,26 +55,83 @@ type InstantiateWasmCallBack = (imports: WebAssembly.Imports, successCallback: I declare type TypedArray = Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array; interface DotnetHostBuilder { + /** + * @param config default values for the runtime configuration. It will be merged with the default values. + * Note that if you provide resources and don't provide custom configSrc URL, the blazor.boot.json will be downloaded and applied by default. + */ withConfig(config: MonoConfig): DotnetHostBuilder; + /** + * @param configSrc URL to the configuration file. ./blazor.boot.json is a default config file location. + */ withConfigSrc(configSrc: string): DotnetHostBuilder; + /** + * "command line" arguments for the Main() method. + * @param args + */ withApplicationArguments(...args: string[]): DotnetHostBuilder; + /** + * Sets the environment variable for the "process" + */ withEnvironmentVariable(name: string, value: string): DotnetHostBuilder; + /** + * Sets the environment variables for the "process" + */ withEnvironmentVariables(variables: { [i: string]: string; }): DotnetHostBuilder; + /** + * Sets the "current directory" for the "process" on the virtual file system. + */ withVirtualWorkingDirectory(vfsPath: string): DotnetHostBuilder; + /** + * @param enabled if "true", writes diagnostic messages during runtime startup and execution to the browser console. + */ withDiagnosticTracing(enabled: boolean): DotnetHostBuilder; + /** + * @param level + * level > 0 enables debugging and sets the logging level to debug + * level == 0 disables debugging and enables interpreter optimizations + * level < 0 enables debugging and disables debug logging. + */ withDebugging(level: number): DotnetHostBuilder; + /** + * @param mainAssemblyName Sets the name of the assembly with the Main() method. Default is the same as the .csproj name. + */ withMainAssembly(mainAssemblyName: string): DotnetHostBuilder; + /** + * Supply "command line" arguments for the Main() method from browser query arguments named "arg". Eg. `index.html?arg=A&arg=B&arg=C`. + * @param args + */ withApplicationArgumentsFromQuery(): DotnetHostBuilder; + /** + * Sets application environment, such as "Development", "Staging", "Production", etc. + */ withApplicationEnvironment(applicationEnvironment?: string): DotnetHostBuilder; + /** + * Sets application culture. This is a name specified in the BCP 47 format. See https://tools.ietf.org/html/bcp47 + */ withApplicationCulture(applicationCulture?: string): DotnetHostBuilder; /** * Overrides the built-in boot resource loading mechanism so that boot resources can be fetched * from a custom source, such as an external CDN. */ withResourceLoader(loadBootResource?: LoadBootResourceCallback): DotnetHostBuilder; + /** + * Downloads all the assets but doesn't create the runtime instance. + */ + download(): Promise; + /** + * Starts the runtime and returns promise of the API object. + */ create(): Promise; + /** + * Runs the Main() method of the application and exits the runtime. + * You can provide "command line" arguments for the Main() method using + * - dotnet.withApplicationArguments(["A", "B", "C"]) + * - dotnet.withApplicationArgumentsFromQuery() + * Note: after the runtime exits, it would reject all further calls to the API. + * You can use runMain() if you want to keep the runtime alive. + */ run(): Promise; } type MonoConfig = { @@ -153,11 +194,21 @@ type MonoConfig = { /** * initial number of workers to add to the emscripten pthread pool */ - pthreadPoolSize?: number; + pthreadPoolInitialSize?: number; /** - * If true, the snapshot of runtime's memory will be stored in the browser and used for faster startup next time. Default is false. + * number of unused workers kept in the emscripten pthread pool after startup */ - startupMemoryCache?: boolean; + pthreadPoolUnusedSize?: number; + /** + * If true, a list of the methods optimized by the interpreter will be saved and used for faster startup + * on future runs of the application + */ + interpreterPgo?: boolean; + /** + * Configures how long to wait before saving the interpreter PGO list. If your application takes + * a while to start you should adjust this value. + */ + interpreterPgoSaveDelay?: number; /** * application environment */ @@ -180,16 +231,31 @@ type MonoConfig = { extensions?: { [name: string]: any; }; + /** + * This is initial working directory for the runtime on the virtual file system. Default is "/". + */ + virtualWorkingDirectory?: string; + /** + * This is the arguments to the Main() method of the program when called with dotnet.run() Default is []. + * Note: RuntimeAPI.runMain() and RuntimeAPI.runMainAndExit() will replace this value, if they provide it. + */ + applicationArguments?: string[]; }; -export type ResourceExtensions = { +type ResourceExtensions = { [extensionName: string]: ResourceList; }; -export interface ResourceGroups { +interface ResourceGroups { hash?: string; + fingerprinting?: { + [name: string]: string; + }; + coreAssembly?: ResourceList; assembly?: ResourceList; lazyAssembly?: ResourceList; + corePdb?: ResourceList; pdb?: ResourceList; jsModuleWorker?: ResourceList; + jsModuleGlobalization?: ResourceList; jsModuleNative: ResourceList; jsModuleRuntime: ResourceList; wasmSymbols?: ResourceList; @@ -201,6 +267,9 @@ export interface ResourceGroups { modulesAfterConfigLoaded?: ResourceList; modulesAfterRuntimeReady?: ResourceList; extensions?: ResourceExtensions; + coreVfs?: { + [virtualPath: string]: ResourceList; + }; vfs?: { [virtualPath: string]: ResourceList; }; @@ -208,7 +277,7 @@ export interface ResourceGroups { /** * A "key" is name of the file, a "value" is optional hash for integrity check. */ -export type ResourceList = { +type ResourceList = { [name: string]: string | null | ""; }; /** @@ -223,7 +292,7 @@ export type ResourceList = { * When returned string is not qualified with `./` or absolute URL, it will be resolved against the application base URI. */ type LoadBootResourceCallback = (type: WebAssemblyBootResourceType, name: string, defaultUri: string, integrity: string, behavior: AssetBehaviors) => string | Promise | null | undefined; -export interface LoadingResource { +interface LoadingResource { name: string; url: string; response: Promise; @@ -279,7 +348,7 @@ interface AssetEntry { } type SingleAssetBehaviors = /** - * The binary of the dotnet runtime. + * The binary of the .NET runtime. */ "dotnetwasm" /** @@ -298,10 +367,22 @@ type SingleAssetBehaviors = * The javascript module for emscripten. */ | "js-module-native" + /** + * The javascript module for hybrid globalization. + */ + | "js-module-globalization" /** * Typically blazor.boot.json */ - | "manifest"; + | "manifest" + /** + * The debugging symbols + */ + | "symbols" + /** + * Load segmentation rules file for Hybrid Globalization. + */ + | "segmentation-rules"; type AssetBehaviors = SingleAssetBehaviors | /** * Load asset as a managed resource assembly. @@ -330,11 +411,7 @@ type AssetBehaviors = SingleAssetBehaviors | /** * The javascript module that came from nuget package . */ - | "js-module-library-initializer" - /** - * The javascript module for threads. - */ - | "symbols"; + | "js-module-library-initializer"; declare const enum GlobalizationMode { /** * Load sharded ICU data. @@ -358,7 +435,6 @@ declare const enum GlobalizationMode { Hybrid = "hybrid" } type DotnetModuleConfig = { - disableDotnet6Compatibility?: boolean; config?: MonoConfig; configSrc?: string; onConfigLoaded?: (config: MonoConfig) => void | Promise; @@ -368,56 +444,197 @@ type DotnetModuleConfig = { exports?: string[]; } & Partial; type APIType = { - runMain: (mainAssemblyName: string, args: string[]) => Promise; - runMainAndExit: (mainAssemblyName: string, args: string[]) => Promise; + /** + * Runs the Main() method of the application. + * Note: this will keep the .NET runtime alive and the APIs will be available for further calls. + * @param mainAssemblyName name of the assembly with the Main() method. Optional. Default is the same as the .csproj name. + * @param args command line arguments for the Main() method. Optional. + * @returns exit code of the Main() method. + */ + runMain: (mainAssemblyName?: string, args?: string[]) => Promise; + /** + * Runs the Main() method of the application and exits the runtime. + * Note: after the runtime exits, it would reject all further calls to the API. + * @param mainAssemblyName name of the assembly with the Main() method. Optional. Default is the same as the .csproj name. + * @param args command line arguments for the Main() method. Optional. + * @returns exit code of the Main() method. + */ + runMainAndExit: (mainAssemblyName?: string, args?: string[]) => Promise; + /** + * Exits the runtime. + * Note: after the runtime exits, it would reject all further calls to the API. + * @param code "process" exit code. + * @param reason could be a string or an Error object. + */ + exit: (code: number, reason?: any) => void; + /** + * Sets the environment variable for the "process" + * @param name + * @param value + */ setEnvironmentVariable: (name: string, value: string) => void; + /** + * Returns the [JSExport] methods of the assembly with the given name + * @param assemblyName + */ getAssemblyExports(assemblyName: string): Promise; + /** + * Provides functions which could be imported by the managed code using [JSImport] + * @param moduleName maps to the second parameter of [JSImport] + * @param moduleImports object with functions which could be imported by the managed code. The keys map to the first parameter of [JSImport] + */ setModuleImports(moduleName: string, moduleImports: any): void; + /** + * Returns the configuration object used to start the runtime. + */ getConfig: () => MonoConfig; + /** + * Executes scripts which were loaded during runtime bootstrap. + * You can register the scripts using MonoConfig.resources.modulesAfterConfigLoaded and MonoConfig.resources.modulesAfterRuntimeReady. + */ invokeLibraryInitializers: (functionName: string, args: any[]) => Promise; + /** + * Writes to the WASM linear memory + */ setHeapB32: (offset: NativePointer, value: number | boolean) => void; + /** + * Writes to the WASM linear memory + */ + setHeapB8: (offset: NativePointer, value: number | boolean) => void; + /** + * Writes to the WASM linear memory + */ setHeapU8: (offset: NativePointer, value: number) => void; + /** + * Writes to the WASM linear memory + */ setHeapU16: (offset: NativePointer, value: number) => void; + /** + * Writes to the WASM linear memory + */ setHeapU32: (offset: NativePointer, value: NativePointer | number) => void; + /** + * Writes to the WASM linear memory + */ setHeapI8: (offset: NativePointer, value: number) => void; + /** + * Writes to the WASM linear memory + */ setHeapI16: (offset: NativePointer, value: number) => void; + /** + * Writes to the WASM linear memory + */ setHeapI32: (offset: NativePointer, value: number) => void; + /** + * Writes to the WASM linear memory + */ setHeapI52: (offset: NativePointer, value: number) => void; + /** + * Writes to the WASM linear memory + */ setHeapU52: (offset: NativePointer, value: number) => void; + /** + * Writes to the WASM linear memory + */ setHeapI64Big: (offset: NativePointer, value: bigint) => void; + /** + * Writes to the WASM linear memory + */ setHeapF32: (offset: NativePointer, value: number) => void; + /** + * Writes to the WASM linear memory + */ setHeapF64: (offset: NativePointer, value: number) => void; + /** + * Reads from the WASM linear memory + */ getHeapB32: (offset: NativePointer) => boolean; + /** + * Reads from the WASM linear memory + */ + getHeapB8: (offset: NativePointer) => boolean; + /** + * Reads from the WASM linear memory + */ getHeapU8: (offset: NativePointer) => number; + /** + * Reads from the WASM linear memory + */ getHeapU16: (offset: NativePointer) => number; + /** + * Reads from the WASM linear memory + */ getHeapU32: (offset: NativePointer) => number; + /** + * Reads from the WASM linear memory + */ getHeapI8: (offset: NativePointer) => number; + /** + * Reads from the WASM linear memory + */ getHeapI16: (offset: NativePointer) => number; + /** + * Reads from the WASM linear memory + */ getHeapI32: (offset: NativePointer) => number; + /** + * Reads from the WASM linear memory + */ getHeapI52: (offset: NativePointer) => number; + /** + * Reads from the WASM linear memory + */ getHeapU52: (offset: NativePointer) => number; + /** + * Reads from the WASM linear memory + */ getHeapI64Big: (offset: NativePointer) => bigint; + /** + * Reads from the WASM linear memory + */ getHeapF32: (offset: NativePointer) => number; + /** + * Reads from the WASM linear memory + */ getHeapF64: (offset: NativePointer) => number; + /** + * Returns a short term view of the WASM linear memory. Don't store the reference, don't use it after await. + */ localHeapViewI8: () => Int8Array; + /** + * Returns a short term view of the WASM linear memory. Don't store the reference, don't use it after await. + */ localHeapViewI16: () => Int16Array; + /** + * Returns a short term view of the WASM linear memory. Don't store the reference, don't use it after await. + */ localHeapViewI32: () => Int32Array; + /** + * Returns a short term view of the WASM linear memory. Don't store the reference, don't use it after await. + */ localHeapViewI64Big: () => BigInt64Array; + /** + * Returns a short term view of the WASM linear memory. Don't store the reference, don't use it after await. + */ localHeapViewU8: () => Uint8Array; + /** + * Returns a short term view of the WASM linear memory. Don't store the reference, don't use it after await. + */ localHeapViewU16: () => Uint16Array; + /** + * Returns a short term view of the WASM linear memory. Don't store the reference, don't use it after await. + */ localHeapViewU32: () => Uint32Array; - localHeapViewF32: () => Float32Array; - localHeapViewF64: () => Float64Array; -}; -type RuntimeAPI = { /** - * @deprecated Please use API object instead. See also MONOType in dotnet-legacy.d.ts + * Returns a short term view of the WASM linear memory. Don't store the reference, don't use it after await. */ - MONO: any; + localHeapViewF32: () => Float32Array; /** - * @deprecated Please use API object instead. See also BINDINGType in dotnet-legacy.d.ts + * Returns a short term view of the WASM linear memory. Don't store the reference, don't use it after await. */ - BINDING: any; + localHeapViewF64: () => Float64Array; +}; +type RuntimeAPI = { INTERNAL: any; Module: EmscriptenModule; runtimeId: number; @@ -425,10 +642,19 @@ type RuntimeAPI = { productVersion: string; gitHash: string; buildConfiguration: string; + wasmEnableThreads: boolean; + wasmEnableSIMD: boolean; + wasmEnableExceptionHandling: boolean; }; } & APIType; type ModuleAPI = { + /** + * The builder for the .NET runtime. + */ dotnet: DotnetHostBuilder; + /** + * Terminates the runtime "process" and reject all further calls to the API. + */ exit: (code: number, reason?: any) => void; }; type CreateDotnetRuntimeType = (moduleFactory: DotnetModuleConfig | ((api: RuntimeAPI) => DotnetModuleConfig)) => Promise; @@ -468,4 +694,4 @@ declare global { } declare const createDotnetRuntime: CreateDotnetRuntimeType; -export { AssetBehaviors, AssetEntry, CreateDotnetRuntimeType, DotnetHostBuilder, DotnetModuleConfig, EmscriptenModule, GlobalizationMode, IMemoryView, ModuleAPI, MonoConfig, RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; +export { type AssetBehaviors, type AssetEntry, type CreateDotnetRuntimeType, type DotnetHostBuilder, type DotnetModuleConfig, type EmscriptenModule, GlobalizationMode, type IMemoryView, type ModuleAPI, type MonoConfig, type RuntimeAPI, createDotnetRuntime as default, dotnet, exit }; diff --git a/src/js/test/cs/Test/Test.csproj b/src/js/test/cs/Test/Test.csproj index 9b0e116a..7b2a6c52 100644 --- a/src/js/test/cs/Test/Test.csproj +++ b/src/js/test/cs/Test/Test.csproj @@ -9,7 +9,6 @@ false npx rollup index.js -d ./ -f es -g process,module --output.preserveModules --entryFileNames [name].mjs true - true diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index facb97b6..013b57ee 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -1,5 +1,6 @@ import { describe, expect, it, vi } from "vitest"; import { resolve } from "node:path"; +import { Buffer } from "node:buffer"; import type { BootOptions } from "../cs/Test/bin/sideload"; async function setup() { @@ -33,16 +34,16 @@ describe("boot", () => { expect(config.assets![1].moduleExports).toBeDefined(); expect(config.assets![2].moduleExports).toBeDefined(); }); - it("overrides name to url in multithreading mode", async () => { - const { bootsharp, root } = await setup(); - vi.doMock("../cs/Test/bin/sideload/dotnet.g", () => ({ mt: true })); - const module = await import("../cs/Test/bin/sideload"); - const config = await module.default.dotnet.buildConfig(bootsharp.resources, root); - expect(config.assets![0].name.endsWith("/bin/dotnet.js")).toBeTruthy(); - expect(config.assets![1].name.endsWith("/bin/dotnet.native.js")).toBeTruthy(); - expect(config.assets![2].name.endsWith("/bin/dotnet.runtime.js")).toBeTruthy(); - vi.doUnmock("../cs/Test/bin/sideload/dotnet.g"); - }); + // it("overrides name to url in multithreading mode", async () => { + // const { bootsharp, root } = await setup(); + // vi.doMock("../cs/Test/bin/sideload/dotnet.g", () => ({ mt: true })); + // const module = await import("../cs/Test/bin/sideload"); + // const config = await module.default.dotnet.buildConfig(bootsharp.resources, root); + // expect(config.assets![0].name.endsWith("/bin/dotnet.js")).toBeTruthy(); + // expect(config.assets![1].name.endsWith("/bin/dotnet.native.js")).toBeTruthy(); + // expect(config.assets![2].name.endsWith("/bin/dotnet.runtime.js")).toBeTruthy(); + // vi.doUnmock("../cs/Test/bin/sideload/dotnet.g"); + // }); it("can boot in embedded mode", async () => { vi.resetModules(); const cs = await import("../cs"); @@ -73,26 +74,29 @@ describe("boot", () => { await bootsharp.boot({ resources, root }); expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); }); - it("can boot with base64 content w/o native encoder available", async () => { + it("uses atob when window is defined in global", async () => { const { bootsharp, Test, root, bins, any } = await setup(); - any(global).Buffer = undefined; + any(global).window = { atob: vi.fn(src => Buffer.from(src, "base64").toString("binary")) }; const resources = { ...bootsharp.resources }; any(resources.wasm).content = bins.wasm.toString("base64"); for (const asm of resources.assemblies) any(asm).content = bins.assemblies.find(a => a.name === asm.name)!.content.toString("base64"); await bootsharp.boot({ resources, root }); + expect(global.window.atob).toHaveBeenCalled(); expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); }); - it("attempts to use atob when window is defined in global", async () => { - const { bootsharp, root, bins, any } = await setup(); - any(global).window = { atob: vi.fn() }; + it("can boot with base64 content w/o native encoder available", async () => { + const { bootsharp, Test, root, bins, any } = await setup(); + const win = any(global).window; + any(global).window = undefined; + any(global).Buffer = undefined; const resources = { ...bootsharp.resources }; any(resources.wasm).content = bins.wasm.toString("base64"); for (const asm of resources.assemblies) any(asm).content = bins.assemblies.find(a => a.name === asm.name)!.content.toString("base64"); - try { await bootsharp.boot({ resources, root }); } - catch {} - expect(global.window.atob).toHaveBeenCalledOnce(); + await bootsharp.boot({ resources, root }); + expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); + any(global).window = win; }); it("throws when boot invoked while booted", async () => { const { bootsharp, root } = await setup(); @@ -135,7 +139,7 @@ describe("boot", () => { }, { name: "dotnet.native.wasm", - buffer: bins.wasm, + buffer: bins.wasm.buffer, behavior: "dotnetwasm" }, ...bins.assemblies.map(a => ({ name: a.name, buffer: a.content, behavior: "assembly" })) From f904a0b1574f748bf925f247bd219494b841a04c Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 3 Jan 2025 17:15:19 +0300 Subject: [PATCH 07/16] workaround .net 9 regression --- src/cs/Directory.Build.props | 2 +- src/js/src/config.ts | 35 ++++++++++++++++++----------------- src/js/test/spec/boot.spec.ts | 10 ---------- 3 files changed, 19 insertions(+), 28 deletions(-) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 986603f7..5a985bce 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.4.0-alpha.22 + 0.4.0-alpha.33 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com diff --git a/src/js/src/config.ts b/src/js/src/config.ts index 9c327805..7ecb85a1 100644 --- a/src/js/src/config.ts +++ b/src/js/src/config.ts @@ -10,28 +10,29 @@ export async function buildConfig(resources: BootResources, root?: string): Prom const main = embed ? await getMain() : undefined; const native = embed ? await getNative() : undefined; const runtime = embed ? await getRuntime() : undefined; - // const mt = !embed && (await import("./dotnet.g")).mt; - return { - mainAssemblyName: resources.entryAssemblyName, - assets: [ - buildAsset({ name: "dotnet.js" }, "js-module-dotnet", main, false), - buildAsset({ name: "dotnet.native.js" }, "js-module-native", native, false), - buildAsset({ name: "dotnet.runtime.js" }, "js-module-runtime", runtime, false), - buildAsset({ name: "dotnet.native.worker.js" }, "js-module-threads", undefined, true), - buildAsset(resources.wasm, "dotnetwasm", undefined, false), - ...resources.assemblies.map(a => buildAsset(a, "assembly")) - ] - }; + const mt = !embed && (await import("./dotnet.g")).mt; + const assets: AssetEntry[] = [ + buildAsset({ name: "dotnet.js" }, "js-module-dotnet", main), + buildAsset({ name: "dotnet.native.js" }, "js-module-native", native), + buildAsset({ name: "dotnet.runtime.js" }, "js-module-runtime", runtime), + buildAsset(resources.wasm, "dotnetwasm", undefined), + ...resources.assemblies.map(a => buildAsset(a, "assembly")) + ]; + if (mt) assets.push(buildAsset({ name: "dotnet.native.worker.js" }, "js-module-threads", undefined)); + return { mainAssemblyName: resources.entryAssemblyName, assets }; - function buildAsset(res: BinaryResource, behavior: AssetBehaviors, - module?: unknown, optional?: boolean): AssetEntry { + function buildAsset(res: BinaryResource, behavior: AssetBehaviors, module?: unknown): AssetEntry { return { name: res.name, - resolvedUrl: (res.content || !root) ? undefined : `${root}/${res.name}`, - buffer: typeof res.content === "string" ? decodeBase64(res.content) : res.content?.buffer, + buffer: module ? undefined : resolveBuffer(res), moduleExports: module, - isOptional: optional, behavior }; } + + async function resolveBuffer(res: BinaryResource): Promise { + if (typeof res.content === "string") return decodeBase64(res.content); + if (res.content !== undefined) return res.content.buffer; + return (await fetch(`${root}/${res.name}`)).arrayBuffer(); + } } diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index 013b57ee..d124e1d3 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -34,16 +34,6 @@ describe("boot", () => { expect(config.assets![1].moduleExports).toBeDefined(); expect(config.assets![2].moduleExports).toBeDefined(); }); - // it("overrides name to url in multithreading mode", async () => { - // const { bootsharp, root } = await setup(); - // vi.doMock("../cs/Test/bin/sideload/dotnet.g", () => ({ mt: true })); - // const module = await import("../cs/Test/bin/sideload"); - // const config = await module.default.dotnet.buildConfig(bootsharp.resources, root); - // expect(config.assets![0].name.endsWith("/bin/dotnet.js")).toBeTruthy(); - // expect(config.assets![1].name.endsWith("/bin/dotnet.native.js")).toBeTruthy(); - // expect(config.assets![2].name.endsWith("/bin/dotnet.runtime.js")).toBeTruthy(); - // vi.doUnmock("../cs/Test/bin/sideload/dotnet.g"); - // }); it("can boot in embedded mode", async () => { vi.resetModules(); const cs = await import("../cs"); From c7f2f4b3422a5ca1a740f13aaea3019d15f3756b Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 3 Jan 2025 18:37:49 +0300 Subject: [PATCH 08/16] fix ci --- .github/workflows/codeql.yml | 2 +- .github/workflows/package.yml | 2 +- .github/workflows/test.yml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index ccc6fd0c..fd1db719 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -24,7 +24,7 @@ jobs: - name: setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: 8 + dotnet-version: 9 - name: initialize codeql uses: github/codeql-action/init@v2 with: diff --git a/.github/workflows/package.yml b/.github/workflows/package.yml index cdc552cd..3737dfe8 100644 --- a/.github/workflows/package.yml +++ b/.github/workflows/package.yml @@ -14,7 +14,7 @@ jobs: - name: setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: 8 + dotnet-version: 9 - name: package run: | cd src/js diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4424d2f6..19047cff 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -14,10 +14,10 @@ jobs: - name: setup dotnet uses: actions/setup-dotnet@v4 with: - dotnet-version: 8 + dotnet-version: 9 - uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 - name: cover run: | cd src/js From a1901880647401effd5a980e855c4fee3cf674d1 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Fri, 3 Jan 2025 20:56:13 +0300 Subject: [PATCH 09/16] iteration --- src/js/src/config.ts | 9 ++++- src/js/src/decoder.ts | 4 +-- src/js/test/cs.ts | 3 +- src/js/test/spec/boot.spec.ts | 62 +++++++++++++++++++---------------- 4 files changed, 44 insertions(+), 34 deletions(-) diff --git a/src/js/src/config.ts b/src/js/src/config.ts index 7ecb85a1..2698b59b 100644 --- a/src/js/src/config.ts +++ b/src/js/src/config.ts @@ -33,6 +33,13 @@ export async function buildConfig(resources: BootResources, root?: string): Prom async function resolveBuffer(res: BinaryResource): Promise { if (typeof res.content === "string") return decodeBase64(res.content); if (res.content !== undefined) return res.content.buffer; - return (await fetch(`${root}/${res.name}`)).arrayBuffer(); + const fullPath = `${root}/${res.name}`; + if (typeof window !== "undefined") { + return (await fetch(fullPath)).arrayBuffer(); + } else { + const { readFile } = await import("fs/promises"); + const bin = await readFile(fullPath); + return bin.buffer.slice(bin.byteOffset, bin.byteOffset + bin.byteLength); + } } } diff --git a/src/js/src/decoder.ts b/src/js/src/decoder.ts index 93d2e9f2..5928333a 100644 --- a/src/js/src/decoder.ts +++ b/src/js/src/decoder.ts @@ -2,7 +2,7 @@ const lookup = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 export function decodeBase64(source: string): ArrayBuffer { if (typeof window === "object") return decodeWithBrowser(source); - if (typeof Buffer === "function") return decodeWithNodeJS(source); + if (typeof process !== "undefined") return decodeWithNode(source); return decodeNaive(source); } @@ -16,7 +16,7 @@ function decodeWithBrowser(source: string): ArrayBuffer { return buffer; } -function decodeWithNodeJS(source: string): ArrayBuffer { +function decodeWithNode(source: string): ArrayBuffer { const buffer = Buffer.from(source, "base64"); return buffer.buffer.slice(buffer.byteOffset, buffer.byteOffset + buffer.byteLength); } diff --git a/src/js/test/cs.ts b/src/js/test/cs.ts index 8b7c6e17..ffba6921 100644 --- a/src/js/test/cs.ts +++ b/src/js/test/cs.ts @@ -3,13 +3,12 @@ import sid, { Test as SidTest } from "./cs/Test/bin/sideload"; import assert from "node:assert"; import { resolve, parse, basename } from "node:path"; import { readdirSync, readFileSync, existsSync } from "node:fs"; -import { pathToFileURL } from "node:url"; export const embedded = emb; export const sideload = sid; export const EmbeddedTest = EmbTest; export const SideloadTest = SidTest; -export const root = pathToFileURL("./test/cs/Test/bin/sideload/bin").toString(); +export const root = "./test/cs/Test/bin/sideload/bin"; export * from "./cs/Test/bin/sideload"; diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index d124e1d3..247cae36 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -27,9 +27,8 @@ describe("boot", () => { expect((await bootsharp.dotnet.getRuntime(root)).embedded).toBeUndefined(); }); it("defines module exports when root is not specified", async () => { - const { bootsharp } = await setup(); - const module = await import("../cs/Test/bin/sideload"); - const config = await module.default.dotnet.buildConfig(bootsharp.resources); + const module = await import("../cs/Test/bin/embedded"); + const config = await module.default.dotnet.buildConfig(module.default.resources); expect(config.assets![0].moduleExports).toBeDefined(); expect(config.assets![1].moduleExports).toBeDefined(); expect(config.assets![2].moduleExports).toBeDefined(); @@ -64,22 +63,25 @@ describe("boot", () => { await bootsharp.boot({ resources, root }); expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); }); - it("uses atob when window is defined in global", async () => { - const { bootsharp, Test, root, bins, any } = await setup(); - any(global).window = { atob: vi.fn(src => Buffer.from(src, "base64").toString("binary")) }; - const resources = { ...bootsharp.resources }; - any(resources.wasm).content = bins.wasm.toString("base64"); - for (const asm of resources.assemblies) - any(asm).content = bins.assemblies.find(a => a.name === asm.name)!.content.toString("base64"); - await bootsharp.boot({ resources, root }); - expect(global.window.atob).toHaveBeenCalled(); - expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); - }); + // it("uses atob when window is defined in global", async () => { + // const { bootsharp, Test, root, bins, any } = await setup(); + // const win = any(global).window; + // any(global).window = { atob: vi.fn(src => Buffer.from(src, "base64").toString("binary")) }; + // const resources = { ...bootsharp.resources }; + // any(resources.wasm).content = bins.wasm.toString("base64"); + // for (const asm of resources.assemblies) + // any(asm).content = bins.assemblies.find(a => a.name === asm.name)!.content.toString("base64"); + // await bootsharp.boot({ resources, root }); + // expect(global.window.atob).toHaveBeenCalled(); + // expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); + // any(global).window = win; + // }); it("can boot with base64 content w/o native encoder available", async () => { const { bootsharp, Test, root, bins, any } = await setup(); const win = any(global).window; + const proc = any(global).process; any(global).window = undefined; - any(global).Buffer = undefined; + any(global).process = undefined; const resources = { ...bootsharp.resources }; any(resources.wasm).content = bins.wasm.toString("base64"); for (const asm of resources.assemblies) @@ -87,6 +89,7 @@ describe("boot", () => { await bootsharp.boot({ resources, root }); expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); any(global).window = win; + any(global).process = proc; }); it("throws when boot invoked while booted", async () => { const { bootsharp, root } = await setup(); @@ -103,12 +106,12 @@ describe("boot", () => { const { bootsharp } = await setup(); await expect(bootsharp.exit).rejects.toThrow(/not booted/); }); - it("can exit when booted", async () => { - const { bootsharp, root } = await setup(); - await bootsharp.boot({ root }); - await bootsharp.exit(); - expect(bootsharp.getStatus()).toStrictEqual(0); - }); + // it("can exit when booted", async () => { + // const { bootsharp, root } = await setup(); + // await bootsharp.boot({ root }); + // await bootsharp.exit(); + // expect(bootsharp.getStatus()).toStrictEqual(0); + // }); it("respects boot customs", async () => { const { bootsharp, bins, root } = await setup(); const customs: BootOptions = { @@ -159,7 +162,8 @@ describe("boot", () => { const runtime = await dotnet.withConfig(cfg).create(); runtime.getAssemblyExports = () => Promise.resolve({}); return runtime; - }) + }), + root }; await bootsharp.boot(options); }); @@ -177,11 +181,11 @@ describe("boot status", () => { await promise; expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); }); - it("transitions to standby on exit", async () => { - const { bootsharp, root } = await setup(); - await bootsharp.boot({ root }); - expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); - await bootsharp.exit(); - expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Standby); - }); + // it("transitions to standby on exit", async () => { + // const { bootsharp, root } = await setup(); + // await bootsharp.boot({ root }); + // expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); + // await bootsharp.exit(); + // expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Standby); + // }); }); From d81fbbc85da317171b70a6af448cd1240a69cfc7 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 4 Jan 2025 18:57:29 +0300 Subject: [PATCH 10/16] fix tests --- src/cs/Directory.Build.props | 2 +- src/js/scripts/cover.sh | 2 +- src/js/scripts/test.sh | 2 +- src/js/src/boot.ts | 5 +- src/js/src/config.ts | 61 ++++++++----- src/js/src/decoder.ts | 2 +- src/js/test/spec/boot.spec.ts | 159 +++++++++++++++++++++------------- 7 files changed, 144 insertions(+), 89 deletions(-) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 5a985bce..3f2b9b68 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.4.0-alpha.33 + 0.4.0-alpha.37 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com diff --git a/src/js/scripts/cover.sh b/src/js/scripts/cover.sh index 7c58a29c..822b6936 100644 --- a/src/js/scripts/cover.sh +++ b/src/js/scripts/cover.sh @@ -1,3 +1,3 @@ -node --expose-gc ./node_modules/vitest/vitest.mjs run --silent --pool=vmThreads \ +node --expose-gc ./node_modules/vitest/vitest.mjs run --silent \ --coverage.enabled --coverage.thresholds.100 --coverage.include=**/sideload/*.mjs \ --coverage.exclude=**/dotnet.* --coverage.allowExternal diff --git a/src/js/scripts/test.sh b/src/js/scripts/test.sh index b2af41f7..201a5380 100644 --- a/src/js/scripts/test.sh +++ b/src/js/scripts/test.sh @@ -1 +1 @@ -node --expose-gc ./node_modules/vitest/vitest.mjs run --silent --pool=vmThreads +node --expose-gc ./node_modules/vitest/vitest.mjs run --silent diff --git a/src/js/src/boot.ts b/src/js/src/boot.ts index d363f628..6d620230 100644 --- a/src/js/src/boot.ts +++ b/src/js/src/boot.ts @@ -62,6 +62,7 @@ export async function boot(options?: BootOptions): Promise { * @param reason Exit reason description (optional). */ export async function exit(code?: number, reason?: string): Promise { if (status !== BootStatus.Booted) throw Error("Failed to exit .NET runtime: not booted."); - main!.exit(code ?? 0, reason); - status = BootStatus.Standby; + try { main?.exit(code ?? 0, reason); } + catch { } + finally { status = BootStatus.Standby; } } diff --git a/src/js/src/config.ts b/src/js/src/config.ts index 2698b59b..6f7d4d6e 100644 --- a/src/js/src/config.ts +++ b/src/js/src/config.ts @@ -7,39 +7,58 @@ import { decodeBase64 } from "./decoder"; * @param root When specified, assumes boot resources are side-loaded from the specified root. */ export async function buildConfig(resources: BootResources, root?: string): Promise { const embed = root == null; - const main = embed ? await getMain() : undefined; - const native = embed ? await getNative() : undefined; - const runtime = embed ? await getRuntime() : undefined; + const assets: AssetEntry[] = await Promise.all([ + resolveWasm(), + resolveModule("dotnet.js", "js-module-dotnet", embed ? getMain : undefined), + resolveModule("dotnet.native.js", "js-module-native", embed ? getNative : undefined), + resolveModule("dotnet.runtime.js", "js-module-runtime", embed ? getRuntime : undefined), + ...resources.assemblies.map(resolveAssembly) + ]); const mt = !embed && (await import("./dotnet.g")).mt; - const assets: AssetEntry[] = [ - buildAsset({ name: "dotnet.js" }, "js-module-dotnet", main), - buildAsset({ name: "dotnet.native.js" }, "js-module-native", native), - buildAsset({ name: "dotnet.runtime.js" }, "js-module-runtime", runtime), - buildAsset(resources.wasm, "dotnetwasm", undefined), - ...resources.assemblies.map(a => buildAsset(a, "assembly")) - ]; - if (mt) assets.push(buildAsset({ name: "dotnet.native.worker.js" }, "js-module-threads", undefined)); + if (mt) assets.push(await resolveModule("dotnet.native.worker.js", "js-module-threads")); return { mainAssemblyName: resources.entryAssemblyName, assets }; - function buildAsset(res: BinaryResource, behavior: AssetBehaviors, module?: unknown): AssetEntry { + async function resolveWasm(): Promise { return { - name: res.name, - buffer: module ? undefined : resolveBuffer(res), - moduleExports: module, + name: resources.wasm.name, + buffer: await resolveBuffer(resources.wasm), + behavior: "dotnetwasm" + }; + } + + async function resolveModule(name: string, behavior: AssetBehaviors, + embed?: () => Promise): Promise { + return { + name, + moduleExports: embed ? await embed() : undefined, behavior }; } + async function resolveAssembly(res: BinaryResource): Promise { + return { + name: res.name, + buffer: await resolveBuffer(res), + behavior: "assembly" + }; + } + async function resolveBuffer(res: BinaryResource): Promise { if (typeof res.content === "string") return decodeBase64(res.content); if (res.content !== undefined) return res.content.buffer; - const fullPath = `${root}/${res.name}`; - if (typeof window !== "undefined") { - return (await fetch(fullPath)).arrayBuffer(); - } else { + if (!embed) return fetchBuffer(res); + throw Error(`Failed to resolve '${res.name}' boot resource.`); + } + + async function fetchBuffer(res: BinaryResource): Promise { + const path = `${root}/${res.name}`; + if (typeof window === "object") + return (await fetch(path)).arrayBuffer(); + if (typeof process === "object") { const { readFile } = await import("fs/promises"); - const bin = await readFile(fullPath); - return bin.buffer.slice(bin.byteOffset, bin.byteOffset + bin.byteLength); + const bin = await readFile(path); + return bin.buffer.slice(bin.byteOffset, bin.byteOffset + bin.byteLength); } + throw Error(`Failed to fetch '${path}' boot resource: unsupported runtime.`); } } diff --git a/src/js/src/decoder.ts b/src/js/src/decoder.ts index 5928333a..f90c8d33 100644 --- a/src/js/src/decoder.ts +++ b/src/js/src/decoder.ts @@ -2,7 +2,7 @@ const lookup = new Uint8Array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 export function decodeBase64(source: string): ArrayBuffer { if (typeof window === "object") return decodeWithBrowser(source); - if (typeof process !== "undefined") return decodeWithNode(source); + if (typeof process === "object") return decodeWithNode(source); return decodeNaive(source); } diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index 247cae36..2c9bf041 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -2,6 +2,7 @@ import { describe, expect, it, vi } from "vitest"; import { resolve } from "node:path"; import { Buffer } from "node:buffer"; import type { BootOptions } from "../cs/Test/bin/sideload"; +import { arrayBuffer } from "node:stream/consumers"; async function setup() { // dotnet merges with the host node process, so it's not possible @@ -10,43 +11,73 @@ async function setup() { vi.resetModules(); const cs = await import("../cs"); cs.SideloadTest.Program.onMainInvoked = vi.fn(); - return { ...cs, bootsharp: cs.sideload, Test: cs.SideloadTest }; + cs.EmbeddedTest.Program.onMainInvoked = vi.fn(); + return { + ...cs, + side: { bootsharp: cs.sideload, Test: cs.SideloadTest }, + embed: { bootsharp: cs.embedded, Test: cs.EmbeddedTest } + }; } describe("boot", () => { it("uses embedded modules when root is not specified", async () => { - const { bootsharp } = await setup(); + const { side: { bootsharp } } = await setup(); expect((await bootsharp.dotnet.getMain()).embedded).toStrictEqual(false); expect((await bootsharp.dotnet.getNative()).embedded).toStrictEqual(false); expect((await bootsharp.dotnet.getRuntime()).embedded).toStrictEqual(false); }); it("uses sideload modules when root is specified", async () => { - const { bootsharp, root } = await setup(); + const { side: { bootsharp }, root } = await setup(); expect((await bootsharp.dotnet.getMain(root)).embedded).toBeUndefined(); expect((await bootsharp.dotnet.getNative(root)).embedded).toBeUndefined(); expect((await bootsharp.dotnet.getRuntime(root)).embedded).toBeUndefined(); }); it("defines module exports when root is not specified", async () => { - const module = await import("../cs/Test/bin/embedded"); - const config = await module.default.dotnet.buildConfig(module.default.resources); - expect(config.assets![0].moduleExports).toBeDefined(); - expect(config.assets![1].moduleExports).toBeDefined(); - expect(config.assets![2].moduleExports).toBeDefined(); - }); - it("can boot in embedded mode", async () => { - vi.resetModules(); - const cs = await import("../cs"); - cs.EmbeddedTest.Program.onMainInvoked = vi.fn(); - await cs.embedded.boot({}); - expect(cs.EmbeddedTest.Program.onMainInvoked).toHaveBeenCalledOnce(); - }); - it("can boot while streaming bins from root", async () => { - const { bootsharp, root, Test } = await setup(); + const { embed: { bootsharp } } = await setup(); + const config = await bootsharp.dotnet.buildConfig(bootsharp.resources); + expect(config.assets!.find(a => a.behavior === "js-module-dotnet")!.moduleExports).toBeDefined(); + expect(config.assets!.find(a => a.behavior === "js-module-native")!.moduleExports).toBeDefined(); + expect(config.assets!.find(a => a.behavior === "js-module-runtime")!.moduleExports).toBeDefined(); + }); + it("throws when missing boot resource", async () => { + const { side: { bootsharp } } = await setup(); + await expect(bootsharp.dotnet.buildConfig(bootsharp.resources)) + .rejects.toThrowError(/Failed to resolve '.+' boot resource/); + }); + it("throws when attempting to fetch boot resource in sandbox", async () => { + const { side: { bootsharp }, root, any } = await setup(); + const win = any(global).window; + const proc = any(global).process; + any(global).window = undefined; + any(global).process = undefined; + await expect(bootsharp.dotnet.buildConfig(bootsharp.resources, root)) + .rejects.toThrowError(/Failed to fetch '.+' boot resource: unsupported runtime/); + any(global).window = win; + any(global).process = proc; + }); + it("uses fetch when fetching boot resource in browser", async () => { + const { side: { bootsharp }, root, any, bins } = await setup(); + const win = any(global).window; + const fetch = global.fetch; + any(global).window = {}; + any(global).fetch = vi.fn(_ => ({ arrayBuffer: () => bins.wasm })); + await bootsharp.dotnet.buildConfig(bootsharp.resources, root); + expect(global.fetch).toHaveBeenCalled(); + any(global).window = win; + any(global).fetch = fetch; + }); + it("can boot with embedded resources", async () => { + const { embed: { bootsharp, Test } } = await setup(); + await bootsharp.boot({}); + expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); + }); + it("can boot while streaming resources from root", async () => { + const { side: { bootsharp }, root, Test } = await setup(); await bootsharp.boot({ root }); expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); }); - it("can boot with bins content pre-assigned", async () => { - const { bootsharp, Test, root, bins, any } = await setup(); + it("can boot with resources content pre-assigned", async () => { + const { side: { bootsharp }, Test, root, bins, any } = await setup(); const resources = { ...bootsharp.resources }; any(resources.wasm).content = bins.wasm; for (const asm of resources.assemblies) @@ -55,7 +86,7 @@ describe("boot", () => { expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); }); it("can boot with base64 content", async () => { - const { bootsharp, Test, root, bins, any } = await setup(); + const { side: { bootsharp }, Test, root, bins, any } = await setup(); const resources = { ...bootsharp.resources }; any(resources.wasm).content = bins.wasm.toString("base64"); for (const asm of resources.assemblies) @@ -63,57 +94,54 @@ describe("boot", () => { await bootsharp.boot({ resources, root }); expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); }); - // it("uses atob when window is defined in global", async () => { - // const { bootsharp, Test, root, bins, any } = await setup(); - // const win = any(global).window; - // any(global).window = { atob: vi.fn(src => Buffer.from(src, "base64").toString("binary")) }; - // const resources = { ...bootsharp.resources }; - // any(resources.wasm).content = bins.wasm.toString("base64"); - // for (const asm of resources.assemblies) - // any(asm).content = bins.assemblies.find(a => a.name === asm.name)!.content.toString("base64"); - // await bootsharp.boot({ resources, root }); - // expect(global.window.atob).toHaveBeenCalled(); - // expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); - // any(global).window = win; - // }); - it("can boot with base64 content w/o native encoder available", async () => { - const { bootsharp, Test, root, bins, any } = await setup(); + it("uses atob when window is defined in global", async () => { + const { bins, any } = await setup(); + const win = any(global).window; + any(global).window = { atob: vi.fn(src => Buffer.from(src, "base64").toString("binary")) }; + // @ts-ignore + const { decodeBase64 } = await import("../cs/Test/bin/sideload/decoder.mjs"); + try { decodeBase64(bins.assemblies[0].content.toString("base64")); } + catch {} + expect(global.window.atob).toHaveBeenCalled(); + any(global).window = win; + }); + it("uses naive decoder when neither window nor process are defined", async () => { + const { bins, any } = await setup(); const win = any(global).window; const proc = any(global).process; any(global).window = undefined; any(global).process = undefined; - const resources = { ...bootsharp.resources }; - any(resources.wasm).content = bins.wasm.toString("base64"); - for (const asm of resources.assemblies) - any(asm).content = bins.assemblies.find(a => a.name === asm.name)!.content.toString("base64"); - await bootsharp.boot({ resources, root }); - expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); + // @ts-ignore + const { decodeBase64 } = await import("../cs/Test/bin/sideload/decoder.mjs"); + for (const ass of bins.assemblies) + expect(decodeBase64(ass.content.toString("base64")).byteLength) + .toStrictEqual(ass.content.length); any(global).window = win; any(global).process = proc; }); it("throws when boot invoked while booted", async () => { - const { bootsharp, root } = await setup(); + const { side: { bootsharp }, root } = await setup(); await bootsharp.boot({ root }); await expect(bootsharp.boot).rejects.toThrow(/already booted/); }); it("throws when boot invoked while booting", async () => { - const { bootsharp, root } = await setup(); + const { side: { bootsharp }, root } = await setup(); const boot = bootsharp.boot({ root }); await expect(bootsharp.boot).rejects.toThrow(/already booting/); await boot; }); it("throws when exit invoked while not booted", async () => { - const { bootsharp } = await setup(); + const { side: { bootsharp } } = await setup(); await expect(bootsharp.exit).rejects.toThrow(/not booted/); }); - // it("can exit when booted", async () => { - // const { bootsharp, root } = await setup(); - // await bootsharp.boot({ root }); - // await bootsharp.exit(); - // expect(bootsharp.getStatus()).toStrictEqual(0); - // }); + it("can exit when booted", async () => { + const { side: { bootsharp }, root } = await setup(); + await bootsharp.boot({ root }); + await bootsharp.exit(); + expect(bootsharp.getStatus()).toStrictEqual(0); + }); it("respects boot customs", async () => { - const { bootsharp, bins, root } = await setup(); + const { side: { bootsharp }, bins, root } = await setup(); const customs: BootOptions = { config: { mainAssemblyName: bins.entryAssemblyName, @@ -154,7 +182,7 @@ describe("boot", () => { expect(customs.export).toHaveBeenCalledOnce(); }); it("can boot when program has no exports", async () => { - const { bootsharp, root } = await setup(); + const { side: { bootsharp }, root } = await setup(); const options: BootOptions = { create: vi.fn(async () => { const cfg = await bootsharp.dotnet.buildConfig(bootsharp.resources, root); @@ -167,25 +195,32 @@ describe("boot", () => { }; await bootsharp.boot(options); }); + it("resolves worker module in multithreading mode", async () => { + const { side: { bootsharp }, root } = await setup(); + vi.doMock("../cs/Test/bin/sideload/dotnet.g", () => ({ mt: true })); + const config = await bootsharp.dotnet.buildConfig(bootsharp.resources, root); + expect(config.assets!.some(a => a.behavior === "js-module-threads")).toBeTruthy(); + vi.doUnmock("../cs/Test/bin/sideload/dotnet.g"); + }); }); describe("boot status", () => { it("is standby by default", async () => { - const { bootsharp } = await setup(); + const { side: { bootsharp } } = await setup(); expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Standby); }); it("transitions to booting and then to booted", async () => { - const { bootsharp, root } = await setup(); + const { side: { bootsharp }, root } = await setup(); const promise = bootsharp.boot({ root }); expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booting); await promise; expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); }); - // it("transitions to standby on exit", async () => { - // const { bootsharp, root } = await setup(); - // await bootsharp.boot({ root }); - // expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); - // await bootsharp.exit(); - // expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Standby); - // }); + it("transitions to standby on exit", async () => { + const { side: { bootsharp }, root } = await setup(); + await bootsharp.boot({ root }); + expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); + await bootsharp.exit(); + expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Standby); + }); }); From 14c57eb2da095e95f65c9fec971ad4c80b5fc3f9 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 4 Jan 2025 19:39:31 +0300 Subject: [PATCH 11/16] update patcher --- .../Pack/ModulePatcher/InternalPatcher.cs | 1 - .../Pack/ModulePatcher/ModulePatcher.cs | 9 +++++++++ src/cs/Directory.Build.props | 2 +- src/js/scripts/cover.sh | 2 +- src/js/scripts/test.sh | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/InternalPatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher/InternalPatcher.cs index 9c005662..8bf75f07 100644 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/InternalPatcher.cs +++ b/src/cs/Bootsharp.Publish/Pack/ModulePatcher/InternalPatcher.cs @@ -20,7 +20,6 @@ public void Patch () .Replace("import(", "import(/*@vite-ignore*//*webpackIgnore:true*/"), Encoding.UTF8); File.WriteAllText(runtime, File.ReadAllText(runtime, Encoding.UTF8) - .Replace("pt('WebAssembly resource does not have the expected content type \"application/wasm\", so falling back to slower ArrayBuffer instantiation.')", "true") .Replace("import(", "import(/*@vite-ignore*//*webpackIgnore:true*/"), Encoding.UTF8); File.WriteAllText(native, File.ReadAllText(native, Encoding.UTF8) diff --git a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs b/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs index 5075cd6e..18c5773e 100644 --- a/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs +++ b/src/cs/Bootsharp.Publish/Pack/ModulePatcher/ModulePatcher.cs @@ -16,9 +16,18 @@ public void Patch () if (thread) PatchThreading(); if (embed) new InternalPatcher(dotnet, runtime, native).Patch(); if (trim) RemoveMaps(); + RemoveWasmNag(); CopyInternals(); } + private void RemoveWasmNag () + { + // Removes "WebAssembly resource does not have the expected content type..." warning. + + File.WriteAllText(dotnet, File.ReadAllText(dotnet, Encoding.UTF8) + .Replace("w('WebAssembly resource does not have the expected content type \"application/wasm\", so falling back to slower ArrayBuffer instantiation.')", "true")); + } + private void PatchThreading () { // Overprotective browser-only assert breaks unit testing: diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index 3f2b9b68..e45b37cc 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.4.0-alpha.37 + 0.4.0-alpha.40 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com diff --git a/src/js/scripts/cover.sh b/src/js/scripts/cover.sh index 822b6936..ff905c4a 100644 --- a/src/js/scripts/cover.sh +++ b/src/js/scripts/cover.sh @@ -1,3 +1,3 @@ -node --expose-gc ./node_modules/vitest/vitest.mjs run --silent \ +node ./node_modules/vitest/vitest.mjs run \ --coverage.enabled --coverage.thresholds.100 --coverage.include=**/sideload/*.mjs \ --coverage.exclude=**/dotnet.* --coverage.allowExternal diff --git a/src/js/scripts/test.sh b/src/js/scripts/test.sh index 201a5380..57337677 100644 --- a/src/js/scripts/test.sh +++ b/src/js/scripts/test.sh @@ -1 +1 @@ -node --expose-gc ./node_modules/vitest/vitest.mjs run --silent +node ./node_modules/vitest/vitest.mjs run From 6fac20a2e3c99886fec458e030131491eba79149 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 4 Jan 2025 19:58:19 +0300 Subject: [PATCH 12/16] codefucktor --- src/cs/Directory.Build.props | 2 +- src/js/src/boot.ts | 15 ++++++++++----- src/js/test/spec/boot.spec.ts | 7 +++---- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index e45b37cc..49b20455 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.4.0-alpha.40 + 0.4.0-alpha.41 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com diff --git a/src/js/src/boot.ts b/src/js/src/boot.ts index 6d620230..ca68d293 100644 --- a/src/js/src/boot.ts +++ b/src/js/src/boot.ts @@ -48,11 +48,7 @@ export async function boot(options?: BootOptions): Promise { if (status === BootStatus.Booting) throw Error("Failed to boot .NET runtime: already booting."); status = BootStatus.Booting; main = await getMain(options?.root); - const config = options?.config ?? await buildConfig(options?.resources ?? resources, options?.root); - const runtime = await options?.create?.(config) || await main.dotnet.withConfig(config).create(); - await options?.import?.(runtime) || bindImports(runtime); - await options?.run?.(runtime) || await runtime.runMain(config.mainAssemblyName!, []); - await options?.export?.(runtime) || await bindExports(runtime, config.mainAssemblyName!); + const runtime = await createRuntime(main, options); status = BootStatus.Booted; return runtime; } @@ -66,3 +62,12 @@ export async function exit(code?: number, reason?: string): Promise { catch { } finally { status = BootStatus.Standby; } } + +async function createRuntime(main: ModuleAPI, opt?: BootOptions) { + const cfg = opt?.config ?? await buildConfig(opt?.resources ?? resources, opt?.root); + const runtime = await opt?.create?.(cfg) || await main.dotnet.withConfig(cfg).create(); + if (opt?.import) await opt.import(runtime); else bindImports(runtime); + if (opt?.run) await opt.run(runtime); else await runtime.runMain(cfg.mainAssemblyName!, []); + if (opt?.export) await opt.export(runtime); else await bindExports(runtime, cfg.mainAssemblyName!); + return runtime; +} diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index 2c9bf041..df87aceb 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -2,7 +2,6 @@ import { describe, expect, it, vi } from "vitest"; import { resolve } from "node:path"; import { Buffer } from "node:buffer"; import type { BootOptions } from "../cs/Test/bin/sideload"; -import { arrayBuffer } from "node:stream/consumers"; async function setup() { // dotnet merges with the host node process, so it's not possible @@ -60,7 +59,7 @@ describe("boot", () => { const win = any(global).window; const fetch = global.fetch; any(global).window = {}; - any(global).fetch = vi.fn(_ => ({ arrayBuffer: () => bins.wasm })); + any(global).fetch = vi.fn(() => ({ arrayBuffer: () => bins.wasm })); await bootsharp.dotnet.buildConfig(bootsharp.resources, root); expect(global.fetch).toHaveBeenCalled(); any(global).window = win; @@ -98,7 +97,7 @@ describe("boot", () => { const { bins, any } = await setup(); const win = any(global).window; any(global).window = { atob: vi.fn(src => Buffer.from(src, "base64").toString("binary")) }; - // @ts-ignore + // @ts-expect-error const { decodeBase64 } = await import("../cs/Test/bin/sideload/decoder.mjs"); try { decodeBase64(bins.assemblies[0].content.toString("base64")); } catch {} @@ -111,7 +110,7 @@ describe("boot", () => { const proc = any(global).process; any(global).window = undefined; any(global).process = undefined; - // @ts-ignore + // @ts-expect-error const { decodeBase64 } = await import("../cs/Test/bin/sideload/decoder.mjs"); for (const ass of bins.assemblies) expect(decodeBase64(ass.content.toString("base64")).byteLength) From f847397dbf15b4046b57c64bcec90830c069c451 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sat, 4 Jan 2025 20:02:48 +0300 Subject: [PATCH 13/16] so code much facktor --- src/js/test/spec/boot.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index df87aceb..8b81e95c 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -97,7 +97,7 @@ describe("boot", () => { const { bins, any } = await setup(); const win = any(global).window; any(global).window = { atob: vi.fn(src => Buffer.from(src, "base64").toString("binary")) }; - // @ts-expect-error + // @ts-expect-error: white-boxing because mocking window breaks other stuff in boot const { decodeBase64 } = await import("../cs/Test/bin/sideload/decoder.mjs"); try { decodeBase64(bins.assemblies[0].content.toString("base64")); } catch {} @@ -110,7 +110,7 @@ describe("boot", () => { const proc = any(global).process; any(global).window = undefined; any(global).process = undefined; - // @ts-expect-error + // @ts-expect-error: white-boxing because mocking proc and window breaks other stuff in boot const { decodeBase64 } = await import("../cs/Test/bin/sideload/decoder.mjs"); for (const ass of bins.assemblies) expect(decodeBase64(ass.content.toString("base64")).byteLength) From c2fc238bec3e1639e0a5a1ca218f7a96ff1b87c5 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 5 Jan 2025 14:39:13 +0300 Subject: [PATCH 14/16] fix mt --- samples/react/backend/Backend.Prime/Options.cs | 2 +- samples/react/backend/Backend.Prime/Prime.cs | 2 +- src/cs/Bootsharp/Build/Bootsharp.targets | 2 +- src/cs/Directory.Build.props | 2 +- src/js/src/config.ts | 4 ++-- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/react/backend/Backend.Prime/Options.cs b/samples/react/backend/Backend.Prime/Options.cs index d720641a..070f5021 100644 --- a/samples/react/backend/Backend.Prime/Options.cs +++ b/samples/react/backend/Backend.Prime/Options.cs @@ -1,3 +1,3 @@ namespace Backend.Prime; -public record Options(int Complexity, bool Multithreading); +public record Options (int Complexity, bool Multithreading); diff --git a/samples/react/backend/Backend.Prime/Prime.cs b/samples/react/backend/Backend.Prime/Prime.cs index b8906e7e..6129c4ae 100644 --- a/samples/react/backend/Backend.Prime/Prime.cs +++ b/samples/react/backend/Backend.Prime/Prime.cs @@ -5,7 +5,7 @@ namespace Backend.Prime; // Implementation of the computer service that compute prime numbers. // Injected in the application entry point assembly (Backend.WASM). -public class Prime(IPrimeUI ui) : IComputer +public class Prime (IPrimeUI ui) : IComputer { private static readonly SemaphoreSlim semaphore = new(0); private readonly Stopwatch watch = new(); diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 8170da11..5f3e4dbe 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -127,7 +127,7 @@ - + - 0.4.0-alpha.41 + 0.4.0-alpha.42 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com diff --git a/src/js/src/config.ts b/src/js/src/config.ts index 6f7d4d6e..7dd9f00e 100644 --- a/src/js/src/config.ts +++ b/src/js/src/config.ts @@ -15,8 +15,8 @@ export async function buildConfig(resources: BootResources, root?: string): Prom ...resources.assemblies.map(resolveAssembly) ]); const mt = !embed && (await import("./dotnet.g")).mt; - if (mt) assets.push(await resolveModule("dotnet.native.worker.js", "js-module-threads")); - return { mainAssemblyName: resources.entryAssemblyName, assets }; + if (mt) assets.push(await resolveModule("dotnet.native.worker.mjs", "js-module-threads")); + return { assets, mainAssemblyName: resources.entryAssemblyName }; async function resolveWasm(): Promise { return { From 39f28eca2eabe3056fb1da37acc4eabcde910942 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 5 Jan 2025 16:52:09 +0300 Subject: [PATCH 15/16] bump ver --- src/cs/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cs/Directory.Build.props b/src/cs/Directory.Build.props index f69b16e3..6437eb84 100644 --- a/src/cs/Directory.Build.props +++ b/src/cs/Directory.Build.props @@ -1,6 +1,6 @@ - 0.4.0-alpha.42 + 0.4.0 Elringus javascript typescript ts js wasm node deno bun interop codegen https://sharp.elringus.com From b7643aa991a59611073b6d74358a305b90be38c4 Mon Sep 17 00:00:00 2001 From: Artyom Sovetnikov <2056864+Elringus@users.noreply.github.com> Date: Sun, 5 Jan 2025 16:58:18 +0300 Subject: [PATCH 16/16] update docs --- docs/guide/build-config.md | 2 +- docs/guide/extensions/dependency-injection.md | 2 +- docs/guide/extensions/file-system.md | 2 +- docs/guide/getting-started.md | 2 +- docs/guide/serialization.md | 2 +- docs/package.json | 10 +++++----- 6 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/guide/build-config.md b/docs/guide/build-config.md index 1cd3965c..28edc312 100644 --- a/docs/guide/build-config.md +++ b/docs/guide/build-config.md @@ -19,7 +19,7 @@ Below is an example configuration, which will make Bootsharp name compiled modul - net8.0 + net9.0 browser-wasm backend $(SolutionDir) diff --git a/docs/guide/extensions/dependency-injection.md b/docs/guide/extensions/dependency-injection.md index 36558101..ae6c7125 100644 --- a/docs/guide/extensions/dependency-injection.md +++ b/docs/guide/extensions/dependency-injection.md @@ -8,7 +8,7 @@ Reference `Bootsharp.Inject` extension in the project configuration: - net8.0 + net9.0 browser-wasm diff --git a/docs/guide/extensions/file-system.md b/docs/guide/extensions/file-system.md index 847c0ec5..6e20c428 100644 --- a/docs/guide/extensions/file-system.md +++ b/docs/guide/extensions/file-system.md @@ -13,7 +13,7 @@ Install the NuGet package to C# project: - net8.0 + net9.0 browser-wasm diff --git a/docs/guide/getting-started.md b/docs/guide/getting-started.md index 8d1f1b2a..4082a6d1 100644 --- a/docs/guide/getting-started.md +++ b/docs/guide/getting-started.md @@ -9,7 +9,7 @@ In `.csproj` file, set wasm runtime identifier and reference Bootsharp package: - net8.0 + net9.0 browser-wasm diff --git a/docs/guide/serialization.md b/docs/guide/serialization.md index 6c789263..cedbb7d7 100644 --- a/docs/guide/serialization.md +++ b/docs/guide/serialization.md @@ -13,7 +13,7 @@ Most simple types, such as numbers, booleans, strings, arrays (lists) and promis | float | Number | ✔️ | ❌ | | DateTime | Date | ✔️ | ❌ | -When a value of non-natively supported type is specified in an interop API, Bootsharp will attempt to de-/serialize it with [System.Text.JSON](https://learn.microsoft.com/en-us/dotnet/api/system.text.json?view=net-8.0) using fast source-generation mode. The whole process is encapsulated under the hood on both the C# and JavaScript sides, so you don't have to manually author generator hints or specify `[MarshallAs]` attributes for each value: +When a value of non-natively supported type is specified in an interop API, Bootsharp will attempt to de-/serialize it with [System.Text.JSON](https://learn.microsoft.com/en-us/dotnet/api/system.text.json) using fast source-generation mode. The whole process is encapsulated under the hood on both the C# and JavaScript sides, so you don't have to manually author generator hints or specify `[MarshallAs]` attributes for each value: ```csharp public record User (long Id, string Name, DateTime Registered); diff --git a/docs/package.json b/docs/package.json index 4079af5f..68c2ca61 100644 --- a/docs/package.json +++ b/docs/package.json @@ -7,10 +7,10 @@ "docs:preview": "vitepress preview" }, "devDependencies": { - "typescript": "^5.6.2", - "@types/node": "^22.5.5", - "vitepress": "^1.3.4", - "typedoc-vitepress-theme": "^1.0.1", - "imgit": "^0.2.1" + "typescript": "5.7.2", + "@types/node": "22.10.5", + "vitepress": "1.5.0", + "typedoc-vitepress-theme": "1.1.1", + "imgit": "0.2.1" } }