diff --git a/docs/guide/build-config.md b/docs/guide/build-config.md index 19ecfdcf..3be3e741 100644 --- a/docs/guide/build-config.md +++ b/docs/guide/build-config.md @@ -2,15 +2,15 @@ Build and publish related options are configured in `.csproj` file via MSBuild properties. -| Property | Default | Description | -|-----------------------------|------------|----------------------------------------------------------------------------------------------------------------------------------| -| BootsharpName | bootsharp | Name of the generated JavaScript module. | -| BootsharpEmbedBinaries | true | Whether to embed binaries to the JavaScript module file. | -| BootsharpBundleCommand | npx rollup | The command to bundle generated JavaScrip solution. | -| BootsharpPublishDirectory | /bin | Directory to publish generated JavaScript module. | -| BootsharpTypesDirectory | /types | Directory to publish type declarations. | -| BootsharpBinariesDirectory | /bin | Directory to publish binaries when `EmbedBinaries` disabled. | -| BootsharpPackageDirectory | / | Directory to publish `package.json` file. | +| Property | Default | Description | +|----------------------------|------------|--------------------------------------------------------------| +| BootsharpName | bootsharp | Name of the generated JavaScript module. | +| BootsharpEmbedBinaries | true | Whether to embed binaries to the JavaScript module file. | +| BootsharpBundleCommand | npx rollup | The command to bundle generated JavaScrip solution. | +| BootsharpPublishDirectory | /bin | Directory to publish generated JavaScript module. | +| BootsharpTypesDirectory | /types | Directory to publish type declarations. | +| BootsharpBinariesDirectory | /bin | Directory to publish binaries when `EmbedBinaries` disabled. | +| BootsharpPackageDirectory | / | Directory to publish `package.json` file. | Below is an example configuration, which will make Bootsharp name compiled module "backend" (instead of the default "bootsharp"), publish the module under solution directory root (instead of "/bin") and disable binaries embedding in favor of publishing them under "public/bin" directory one level above the solution root: @@ -32,3 +32,24 @@ Below is an example configuration, which will make Bootsharp name compiled modul ``` + +## Globalization + +By default, Bootsharp disables .NET globalization on WASM. This keeps the published output smaller, but culture-specific formatting and culture construction will use invariant mode. + +To enable globalization, explicitly disable invariant globalization in your project file: + +```xml + + false + +``` + +When invariant globalization is disabled, Bootsharp will automatically include the ICU files emitted by the .NET WASM build and configure the runtime accordingly. This works for both embedded and sideloaded binaries. + +Bootsharp supports the following globalization modes: + +| Mode | How to enable | Behavior | +|---------|----------------------------------------------------------------------|-------------------------------------------------------------------------------------------| +| Sharded | Didable `InvariantGlobalization` | Publishes the default sharded ICU files (`icudt_*.dat`). | +| Full | Didable `InvariantGlobalization` and enable `WasmIncludeFullIcuData` | Publishes the full ICU data file (`icudt.dat`) and supports many cultures in one runtime. | diff --git a/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs index 5a236306..e3fd4eb1 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/PackTest.cs @@ -45,6 +45,7 @@ protected override void AddAssembly (string assemblyName, params MockSource[] so EntryAssemblyName = "System.Runtime.dll", BuildEngine = Engine, EmbedBinaries = false, + Globalization = false, Threading = false, LLVM = false, Debug = false diff --git a/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs b/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs index c4bc34b5..ef6b7ab1 100644 --- a/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs +++ b/src/cs/Bootsharp.Publish.Test/Pack/ResourceTest.cs @@ -55,4 +55,24 @@ public void WhenDebugDisabledDebugArtifactsNotIncluded () DoesNotContain("""{ name: "Foo.pdb", content: undefined }"""); DoesNotContain("""{ name: "dotnet.native.js.symbols", content: undefined }"""); } + + [Fact] + public void WhenGlobalizationEnabledIcuIncluded () + { + Task.Globalization = true; + AddAssembly("Foo.dll"); + Project.WriteFile("icudt.dat", "MockIcuContent"); + Execute(); + Contains("""{ name: "icudt.dat", content: undefined }"""); + } + + [Fact] + public void WhenGlobalizationDisabledIcuNotIncluded () + { + Task.Globalization = false; + AddAssembly("Foo.dll"); + Project.WriteFile("icudt.dat", "MockIcuContent"); + Execute(); + DoesNotContain("""{ name: "icudt.dat", content: undefined }"""); + } } diff --git a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs index 6db1da68..baec7ecc 100644 --- a/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs +++ b/src/cs/Bootsharp.Publish/Pack/BootsharpPack.cs @@ -10,6 +10,7 @@ public sealed class BootsharpPack : Microsoft.Build.Utilities.Task public required string InspectedDirectory { get; set; } public required string EntryAssemblyName { get; set; } public required bool EmbedBinaries { get; set; } + public required bool Globalization { get; set; } public required bool Threading { get; set; } public required bool LLVM { get; set; } public required bool Debug { get; set; } @@ -64,7 +65,7 @@ private void GenerateDeclarations (Preferences prefs, SolutionInspection inspect private void GenerateResources (SolutionInspection inspection) { - var generator = new ResourceGenerator(EntryAssemblyName, EmbedBinaries, Debug); + var generator = new ResourceGenerator(EntryAssemblyName, EmbedBinaries, Debug, Globalization); var content = generator.Generate(BuildDirectory, DebugDirectory); File.WriteAllText(Path.Combine(BuildDirectory, "resources.g.js"), content); } diff --git a/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs b/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs index 2806eab5..428f1ec3 100644 --- a/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs +++ b/src/cs/Bootsharp.Publish/Pack/ResourceGenerator.cs @@ -1,10 +1,11 @@ namespace Bootsharp.Publish; -internal sealed class ResourceGenerator (string entryAssemblyName, bool embed, bool debug) +internal sealed class ResourceGenerator (string entryAssemblyName, bool embed, bool debug, bool g11n) { private readonly List assemblies = []; private readonly List symbols = []; private readonly List pdb = []; + private readonly List icu = []; private string wasm = null!; public string Generate (string buildDir, string debugDir) @@ -12,6 +13,11 @@ public string Generate (string buildDir, string debugDir) foreach (var path in Directory.GetFiles(buildDir, "*.wasm").Order()) if (path.EndsWith("dotnet.native.wasm")) wasm = BuildBin(path); else assemblies.Add(BuildBin(path)); + if (g11n) + { + foreach (var path in Directory.GetFiles(buildDir, "*.dat").Order()) + icu.Add(BuildBin(path)); + } if (debug) { foreach (var path in Directory.GetFiles(debugDir, "*.symbols").Order()) @@ -26,6 +32,9 @@ public string Generate (string buildDir, string debugDir) assemblies: [ {{JoinLines(assemblies, 2, ",\n")}} ], + icu: [ + {{JoinLines(icu, 2, ",\n")}} + ], symbols: [ {{JoinLines(symbols, 2, ",\n")}} ], diff --git a/src/cs/Bootsharp/Build/Bootsharp.props b/src/cs/Bootsharp/Build/Bootsharp.props index 98fcbeab..3f671d84 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.props +++ b/src/cs/Bootsharp/Build/Bootsharp.props @@ -9,9 +9,10 @@ false true - - true - true + + true + true + true @@ -36,12 +37,16 @@ !$(BsDebug) - - - true - $(EmccFlags) -O3 - false + + + false + false + true + true + true + + @@ -71,7 +76,6 @@ false false false - true false true false @@ -79,23 +83,21 @@ false - - - false - false - true - true - true + + + true + $(EmccFlags) -O3 + false + + + <_Parameter1>browser - - - diff --git a/src/cs/Bootsharp/Build/Bootsharp.targets b/src/cs/Bootsharp/Build/Bootsharp.targets index 27800cbd..8f394f82 100644 --- a/src/cs/Bootsharp/Build/Bootsharp.targets +++ b/src/cs/Bootsharp/Build/Bootsharp.targets @@ -93,8 +93,8 @@ $(BootsharpPublishDirectory)/types $(BootsharpPublishDirectory)/bin $(BootsharpPublishDirectory) - true - false + $([System.String]::Equals('$(InvariantGlobalization)', 'false')) + $([System.String]::Equals('$(WasmEnableThreads)', 'true')) false --sourcemap npx --yes rollup index.js -o index.mjs -f es -e process,module,fs/promises --output.inlineDynamicImports $(BsBundleMapsArg) @@ -123,6 +123,7 @@ InspectedDirectory="$(OutputPath)" EntryAssemblyName="$(BsEntryAssembly)" EmbedBinaries="$(BootsharpEmbedBinaries)" + Globalization="$(BsGlobalization)" Threading="$(BsThreading)" LLVM="$(BsLlvm)" Debug="$(BsDebug)"/> @@ -137,6 +138,7 @@ + @@ -151,6 +153,8 @@ DestinationFolder="$(BootsharpBinariesDirectory)"/> + - 0.8.0-alpha.69 + 0.8.0-alpha.74 Elringus javascript typescript ts js wasm node deno bun interop codegen https://bootsharp.com diff --git a/src/js/src/config.ts b/src/js/src/config.ts index 31ebbe05..3410052e 100644 --- a/src/js/src/config.ts +++ b/src/js/src/config.ts @@ -1,4 +1,4 @@ -import { RuntimeConfig, WasmAsset, ModuleAsset, AssemblyAsset, PdbAsset, SymbolsAsset, getRuntime, getNative } from "./modules"; +import { RuntimeConfig, Asset, WasmAsset, ModuleAsset, AssemblyAsset, IcuAsset, PdbAsset, SymbolsAsset, getRuntime, getNative } from "./modules"; import { BinaryResource, BootResources } from "./resources"; import { decodeBase64 } from "./decoder"; @@ -8,13 +8,14 @@ import { decodeBase64 } from "./decoder"; export async function buildConfig(resources: BootResources, root?: string): Promise { const embed = root == null; const mt = !embed && (await import("./dotnet.g")).mt; - const [wasm, native, runtime, assemblies, symbols, pdb] = await Promise.all([ - resolveWasm(), + const [wasm, native, runtime, assemblies, icu, symbols, pdb] = await Promise.all([ + resolveAsset(resources.wasm), resolveModule("dotnet.native.js", embed ? getNative : undefined), resolveModule("dotnet.runtime.js", embed ? getRuntime : undefined), - Promise.all(resources.assemblies.map(resolveAssembly)), + Promise.all(resources.assemblies.map(resolveAsset)), + Promise.all(resources.icu.map(resolveAsset)), Promise.all(resources.symbols.map(resolveSymbols)), - Promise.all(resources.pdb.map(resolvePdb)) + Promise.all(resources.pdb.map(resolveAsset)) ]); return { resources: { @@ -24,17 +25,18 @@ export async function buildConfig(resources: BootResources, root?: string): Prom jsModuleWorker: mt ? [await resolveModule("dotnet.native.worker.mjs")] : undefined, assembly: assemblies, wasmSymbols: symbols, - pdb: pdb + pdb: pdb, + icu: icu }, mainAssemblyName: resources.entryAssemblyName, + globalizationMode: resolveGlobalizationMode(), debugLevel: resources.symbols.length > 0 ? -1 : undefined }; - async function resolveWasm(): Promise { - return { - name: resources.wasm.name, - buffer: await resolveBuffer(resources.wasm) - }; + function resolveGlobalizationMode(): RuntimeConfig["globalizationMode"] { + if (resources.icu.length === 0) return "invariant"; + if (resources.icu.some(res => res.name === "icudt.dat")) return "all"; + return "sharded"; } async function resolveModule(name: string, embed?: () => Promise): Promise { @@ -44,16 +46,8 @@ export async function buildConfig(resources: BootResources, root?: string): Prom }; } - async function resolveAssembly(res: BinaryResource): Promise { - return { - name: res.name, - virtualPath: res.name, - buffer: await resolveBuffer(res) - }; - } - - async function resolvePdb(res: BinaryResource): Promise { - return { + async function resolveAsset(res: BinaryResource): Promise { + return { name: res.name, virtualPath: res.name, buffer: await resolveBuffer(res) @@ -61,7 +55,7 @@ export async function buildConfig(resources: BootResources, root?: string): Prom } async function resolveSymbols(res: BinaryResource): Promise { - // Use buffer similar to the other assets once https://github.com/dotnet/runtime/pull/127087 is merged. + // Use 'resolveAsset()' once https://github.com/dotnet/runtime/pull/127087 is merged. const txt = new TextDecoder("utf-8").decode(await resolveBuffer(res)); return { name: res.name, diff --git a/src/js/src/dotnet.g.d.ts b/src/js/src/dotnet.g.d.ts index 7dd633b1..2f8aeb3a 100644 --- a/src/js/src/dotnet.g.d.ts +++ b/src/js/src/dotnet.g.d.ts @@ -266,7 +266,7 @@ interface Assets { coreVfs?: VfsAsset[]; vfs?: VfsAsset[]; } -type Asset = { +export type Asset = { /** * this should be absolute url to the asset */ diff --git a/src/js/src/modules.ts b/src/js/src/modules.ts index 02435911..1bfaeb18 100644 --- a/src/js/src/modules.ts +++ b/src/js/src/modules.ts @@ -1,4 +1,5 @@ import type { ModuleAPI, MonoConfig } from "./dotnet.g.d.ts"; +export type { Asset } from "./dotnet.g.d.ts"; export type * from "./dotnet.g.d.ts"; export type RuntimeConfig = MonoConfig; @@ -6,6 +7,7 @@ export type RuntimeResources = NonNullable; export type WasmAsset = NonNullable[number]; export type ModuleAsset = NonNullable[number]; export type AssemblyAsset = NonNullable[number]; +export type IcuAsset = NonNullable[number]; export type PdbAsset = NonNullable[number]; export type SymbolsAsset = NonNullable[number]; diff --git a/src/js/src/resources.ts b/src/js/src/resources.ts index 8a75280d..8ec433f9 100644 --- a/src/js/src/resources.ts +++ b/src/js/src/resources.ts @@ -6,6 +6,8 @@ export type BootResources = { readonly wasm: BinaryResource; /** Compiled .NET assemblies. */ readonly assemblies: BinaryResource[]; + /** Globalization data. */ + readonly icu: BinaryResource[]; /** WASM debug symbols. */ readonly symbols: BinaryResource[]; /** PDB debug artifacts. */ diff --git a/src/js/test/cs/Test/Platform.cs b/src/js/test/cs/Test/Platform.cs index b5612ca0..cd87781b 100644 --- a/src/js/test/cs/Test/Platform.cs +++ b/src/js/test/cs/Test/Platform.cs @@ -1,4 +1,5 @@ using System; +using System.Globalization; using System.Net.WebSockets; using System.Text; using System.Threading; @@ -12,6 +13,13 @@ public static partial class Platform [JSInvokable] public static string GetGuid () => Guid.NewGuid().ToString(); + [JSInvokable] + public static string FormatDate (string culture, int month, int day, string format) + { + var info = new CultureInfo(culture, false); + return new DateTime(2024, month, day).ToString(format, info); + } + [JSInvokable] public static string? CatchException () { diff --git a/src/js/test/cs/Test/Test.csproj b/src/js/test/cs/Test/Test.csproj index 7b4734a3..c937d4a7 100644 --- a/src/js/test/cs/Test/Test.csproj +++ b/src/js/test/cs/Test/Test.csproj @@ -8,6 +8,8 @@ bin/codegen false npx --yes rollup index.js -d ./ -f es -e process,module,fs/promises --output.preserveModules --entryFileNames [name].mjs --sourcemap + false + true diff --git a/src/js/test/spec/boot.spec.ts b/src/js/test/spec/boot.spec.ts index 9cc0ab0d..dd537feb 100644 --- a/src/js/test/spec/boot.spec.ts +++ b/src/js/test/spec/boot.spec.ts @@ -37,17 +37,34 @@ describe("boot", () => { expect(config.resources!.jsModuleNative[0].moduleExports).toBeDefined(); expect(config.resources!.jsModuleRuntime[0].moduleExports).toBeDefined(); }); - it("enables debugging when debugging resources exist", async () => { + it("enables debugging when debugging resources are present", async () => { const { side: { bootsharp }, root } = await setup(); const config = await bootsharp.dotnet.buildConfig(bootsharp.resources, root); expect(config.debugLevel).not.toBeUndefined(); }); - it("doesn't enable debugging when missing debug artifacts", async () => { + it("doesn't enable debugging when debug are absent", async () => { const { side: { bootsharp }, root } = await setup(); const resources = { ...bootsharp.resources, symbols: [], pdb: [] }; const config = await bootsharp.dotnet.buildConfig(resources, root); expect(config.debugLevel).toBeUndefined(); }); + it("uses full globalization mode when full ICU resource is present", async () => { + const { side: { bootsharp }, root } = await setup(); + const config = await bootsharp.dotnet.buildConfig(bootsharp.resources, root); + expect(config.globalizationMode).toStrictEqual("all"); + }); + it("uses sharded globalization mode when sharded ICU resource is present", async () => { + const { side: { bootsharp }, root } = await setup(); + const resources = { ...bootsharp.resources, icu: [{ name: "icudt_CJK.dat", content: new Uint8Array() }] }; + const config = await bootsharp.dotnet.buildConfig(resources, root); + expect(config.globalizationMode).toStrictEqual("sharded"); + }); + it("disables globalization when ICU resources are absent", async () => { + const { side: { bootsharp }, root } = await setup(); + const resources = { ...bootsharp.resources, icu: [] }; + const config = await bootsharp.dotnet.buildConfig(resources, root); + expect(config.globalizationMode).toStrictEqual("invariant"); + }); it("throws when missing boot resource", async () => { const { side: { bootsharp } } = await setup(); await expect(bootsharp.dotnet.buildConfig(bootsharp.resources)) diff --git a/src/js/test/spec/platform.spec.ts b/src/js/test/spec/platform.spec.ts index 42de5cac..ff47a446 100644 --- a/src/js/test/spec/platform.spec.ts +++ b/src/js/test/spec/platform.spec.ts @@ -11,6 +11,11 @@ describe("platform", () => { expect(guid2.length).toStrictEqual(36); expect(guid1).not.toStrictEqual(guid2); }); + it("supports globalization", () => { + expect(Test.Platform.formatDate("ru", 5, 1, "d MMMM")).toStrictEqual("1 мая"); + expect(Test.Platform.formatDate("ja", 5, 1, "d MMMM")).toStrictEqual("1 5月"); + expect(Test.Platform.formatDate("en", 5, 1, "MMMM d")).toStrictEqual("May 1"); + }); it("can communicate via websocket", async () => { // .NET requires ws package when running on node: // https://github.com/dotnet/runtime/blob/main/src/mono/wasm/features.md#websocket