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 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" } } 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.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/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/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/.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 } 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.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); } 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.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/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/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index b2561cb8..5f3e4dbe 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -84,7 +84,7 @@ - @@ -105,8 +105,6 @@ - - + - 0.3.3 + 0.4.0 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 44ec6e62..3d1794ab 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.7.2", + "@types/node": "22.10.2", + "@types/ws": "8.5.13", + "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/scripts/cover.sh b/src/js/scripts/cover.sh index 7c58a29c..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 --pool=vmThreads \ +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 b2af41f7..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 --pool=vmThreads +node ./node_modules/vitest/vitest.mjs run diff --git a/src/js/src/boot.ts b/src/js/src/boot.ts index 1d9405e8..ca68d293 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; @@ -48,13 +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(); - // 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!); + const runtime = await createRuntime(main, options); status = BootStatus.Booted; return runtime; } @@ -64,6 +58,16 @@ 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; } +} + +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/src/config.ts b/src/js/src/config.ts index 2b67ba98..7dd9f00e 100644 --- a/src/js/src/config.ts +++ b/src/js/src/config.ts @@ -7,36 +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; - 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")) - ] - }; + if (mt) assets.push(await resolveModule("dotnet.native.worker.mjs", "js-module-threads")); + return { assets, mainAssemblyName: resources.entryAssemblyName }; - function buildAsset(res: BinaryResource, behavior: AssetBehaviors, - module?: unknown, optional?: boolean): AssetEntry { - const url = `${root}/${res.name}`; + async function resolveWasm(): Promise { 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, - moduleExports: module, - isOptional: optional, + 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; + 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(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 09111f18..f90c8d33 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 process === "object") return decodeWithNode(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 decodeWithNode(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.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/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..7b2a6c52 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 @@ -9,14 +9,13 @@ 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..8b81e95c 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() { @@ -9,54 +10,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 { bootsharp } = await setup(); - const module = await import("../cs/Test/bin/sideload"); - const config = await module.default.dotnet.buildConfig(bootsharp.resources); - expect(config.assets![0].moduleExports).toBeDefined(); - 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"); - cs.EmbeddedTest.Program.onMainInvoked = vi.fn(); - await cs.embedded.boot({}); - expect(cs.EmbeddedTest.Program.onMainInvoked).toHaveBeenCalledOnce(); + 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 bins from root", async () => { - const { bootsharp, root, Test } = await setup(); + 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) @@ -65,7 +85,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) @@ -73,50 +93,54 @@ describe("boot", () => { await bootsharp.boot({ resources, root }); expect(Test.Program.onMainInvoked).toHaveBeenCalledOnce(); }); - it("can boot with base64 content w/o native encoder available", async () => { - const { bootsharp, Test, root, bins, any } = await setup(); - 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"); - await bootsharp.boot({ resources, root }); - 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() }; - 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 }); } + 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-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 {} - expect(global.window.atob).toHaveBeenCalledOnce(); + 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; + // @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) + .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(); + 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, @@ -135,7 +159,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" })) @@ -157,7 +181,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); @@ -165,26 +189,34 @@ describe("boot", () => { const runtime = await dotnet.withConfig(cfg).create(); runtime.getAssemblyExports = () => Promise.resolve({}); return runtime; - }) + }), + root }; 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(); + const { side: { bootsharp }, root } = await setup(); await bootsharp.boot({ root }); expect(bootsharp.getStatus()).toStrictEqual(bootsharp.BootStatus.Booted); await bootsharp.exit();