[browser] introduce sourcemaps (#86152)
authorPavel Savara <pavel.savara@gmail.com>
Wed, 28 Jun 2023 11:43:34 +0000 (13:43 +0200)
committerGitHub <noreply@github.com>
Wed, 28 Jun 2023 11:43:34 +0000 (13:43 +0200)
15 files changed:
eng/liveBuilds.targets
src/installer/pkg/sfx/Microsoft.NETCore.App/Directory.Build.props
src/mono/nuget/Microsoft.NET.Sdk.WebAssembly.Pack/build/Microsoft.NET.Sdk.WebAssembly.Browser.targets
src/mono/wasm/Wasm.Build.Tests/BuildTestBase.cs
src/mono/wasm/Wasm.Build.Tests/NativeRebuildTests/NativeRebuildTestsBase.cs
src/mono/wasm/build/WasmApp.targets
src/mono/wasm/runtime/dotnet.d.ts
src/mono/wasm/runtime/package.json
src/mono/wasm/runtime/rollup.config.js
src/mono/wasm/runtime/tsconfig.json
src/mono/wasm/wasm.proj
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/AssetsComputingHelper.cs
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmBuildAssets.cs
src/tasks/Microsoft.NET.Sdk.WebAssembly.Pack.Tasks/ComputeWasmPublishAssets.cs
src/tasks/WasmAppBuilder/WasmAppBuilder.cs

index 5ea1ca5..855ad32 100644 (file)
       <LibrariesRuntimeFiles Condition="'$(TargetOS)' == 'browser'"
                              Include="
         $(LibrariesNativeArtifactsPath)dotnet.js;
+        $(LibrariesNativeArtifactsPath)dotnet.js.map;
         $(LibrariesNativeArtifactsPath)dotnet.native.js;
         $(LibrariesNativeArtifactsPath)dotnet.runtime.js;
+        $(LibrariesNativeArtifactsPath)dotnet.runtime.js.map;
         $(LibrariesNativeArtifactsPath)dotnet.d.ts;
         $(LibrariesNativeArtifactsPath)dotnet-legacy.d.ts;
         $(LibrariesNativeArtifactsPath)package.json;
index 3a961b3..36ee8d1 100644 (file)
     <PlatformManifestFileEntry Include="libmono-wasm-eh-wasm.a" IsNative="true" />
     <PlatformManifestFileEntry Include="wasm-bundled-timezones.a" IsNative="true" />
     <PlatformManifestFileEntry Include="dotnet.js" IsNative="true" />
+    <PlatformManifestFileEntry Include="dotnet.js.map" IsNative="true" />
     <PlatformManifestFileEntry Include="dotnet.runtime.js" IsNative="true" />
+    <PlatformManifestFileEntry Include="dotnet.runtime.js.map" IsNative="true" />
     <PlatformManifestFileEntry Include="dotnet.native.js" IsNative="true" />
     <PlatformManifestFileEntry Include="dotnet.native.worker.js" IsNative="true" />
     <PlatformManifestFileEntry Include="dotnet.native.js.symbols" IsNative="true" />
index 5cf78e8..4186a7a 100644 (file)
@@ -208,6 +208,11 @@ Copyright (c) .NET Foundation. All rights reserved.
                                Condition="@(WasmNativeAsset->Count()) > 0 and ( '%(FileName)' == 'dotnet' or '%(FileName)' == 'dotnet.native' ) and ('%(Extension)' == '.wasm' or '%(Extension)' == '.js')" />
     </ItemGroup>
 
+    <PropertyGroup>
+      <_WasmEmitSourceMapBuild>$(WasmEmitSourceMap)</_WasmEmitSourceMapBuild>
+      <_WasmEmitSourceMapBuild Condition="'$(_WasmEmitSourceMapBuild)' == ''">true</_WasmEmitSourceMapBuild>
+    </PropertyGroup>
+
     <ComputeWasmBuildAssets
       Candidates="@(ReferenceCopyLocalPaths->Distinct());@(WasmNativeAsset)"
       CustomIcuCandidate="$(_BlazorIcuDataFileName)"
@@ -222,6 +227,7 @@ Copyright (c) .NET Foundation. All rights reserved.
       OutputPath="$(OutputPath)"
       FingerprintDotNetJs="$(WasmFingerprintDotnetJs)"
       EnableThreads="$(_WasmEnableThreads)"
+      EmitSourceMap="$(_WasmEmitSourceMapBuild)"
     >
       <Output TaskParameter="AssetCandidates" ItemName="_BuildAssetsCandidates" />
       <Output TaskParameter="FilesToRemove" ItemName="_WasmBuildFilesToRemove" />
@@ -376,6 +382,11 @@ Copyright (c) .NET Foundation. All rights reserved.
         Condition="'%(StaticWebAsset.AssetTraitName)' == 'WasmResource' or '%(StaticWebAsset.AssetTraitName)' == 'Culture' or '%(AssetRole)' == 'Alternative'" />
     </ItemGroup>
 
+    <PropertyGroup>
+      <_WasmEmitSourceMapPublish>$(WasmEmitSourceMap)</_WasmEmitSourceMapPublish>
+      <_WasmEmitSourceMapPublish Condition="'$(_WasmEmitSourceMapPublish)' == ''">false</_WasmEmitSourceMapPublish>
+    </PropertyGroup>
+
     <ComputeWasmPublishAssets
       ResolvedFilesToPublish="@(ResolvedFileToPublish)"
       CustomIcuCandidate="$(_BlazorIcuDataFileName)"
@@ -388,6 +399,7 @@ Copyright (c) .NET Foundation. All rights reserved.
       DotNetJsVersion="$(_WasmRuntimePackVersion)"
       FingerprintDotNetJs="$(WasmFingerprintDotnetJs)"
       EnableThreads="$(_WasmEnableThreads)"
+      EmitSourceMap="$(_WasmEmitSourceMapPublish)"
       IsWebCilEnabled="$(_WasmEnableWebcil)"
     >
       <Output TaskParameter="NewCandidates" ItemName="_NewWasmPublishStaticWebAssets" />
index 8e0f235..433af91 100644 (file)
@@ -685,8 +685,10 @@ namespace Wasm.Build.Tests
                 "_framework/dotnet.native.wasm",
                 "_framework/blazor.boot.json",
                 "_framework/dotnet.js",
+                "_framework/dotnet.js.map",
                 "_framework/dotnet.native.js",
-                "_framework/dotnet.runtime.js"
+                "_framework/dotnet.runtime.js",
+                "_framework/dotnet.runtime.js.map",
             };
 
             if (isBrowserProject)
index d190dc2..7bdc4c2 100644 (file)
@@ -186,7 +186,9 @@ namespace Wasm.Build.NativeRebuild.Tests
 
             // those files do not change on re-link
             dict["dotnet.js"]=(Path.Combine(paths.BundleDir, "_framework", "dotnet.js"), true);
+            dict["dotnet.js.map"]=(Path.Combine(paths.BundleDir, "_framework", "dotnet.js.map"), true);
             dict["dotnet.runtime.js"]=(Path.Combine(paths.BundleDir, "_framework", "dotnet.runtime.js"), true);
+            dict["dotnet.runtime.js.map"]=(Path.Combine(paths.BundleDir, "_framework", "dotnet.runtime.js.map"), true);
 
             return dict;
         }
index e76fd80..68f6877 100644 (file)
@@ -23,6 +23,7 @@
       - $(WasmNativeDebugSymbols) - Build with native debug symbols, useful only with `$(RunAOTCompilation)`, or `$(WasmBuildNative)`
                                     Defaults to true.
       - $(WasmEmitSymbolMap)      - Generates a `dotnet.native.js.symbols` file with a map of wasm function number to name.
+      - $(WasmEmitSourceMap)      - Generates `dotnet.runtime.js.map` and `dotnet.js.map` files with a TypeScript source map.
       - $(WasmDedup)         - Whenever to dedup generic instances when using AOT. Defaults to true.
 
       - $(WasmProfilers)     - Profilers to use
                        Condition="'$(WasmEmitSymbolMap)' == 'true' and
                                   '$(_HasDotnetJsSymbols)' != 'true' and
                                   Exists('$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.native.js.symbols')" />
+      <WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.js.map" 
+                       Condition="'$(WasmEmitSourceMap)' != 'false'" />
+      <WasmNativeAsset Include="$(MicrosoftNetCoreAppRuntimePackRidNativeDir)dotnet.runtime.js.map" 
+                       Condition="'$(WasmEmitSourceMap)' != 'false'" />
     </ItemGroup>
 
     <ItemGroup Condition="'$(InvariantGlobalization)' != 'true'">
index 50c6324..b715912 100644 (file)
@@ -309,6 +309,7 @@ interface BootJsonData {
     readonly resources: ResourceGroups;
     /** Gets a value that determines if this boot config was produced from a non-published build (i.e. dotnet build or dotnet run) */
     readonly debugBuild: boolean;
+    readonly debugLevel: number;
     readonly linkerEnabled: boolean;
     readonly cacheBootResources: boolean;
     readonly config: string[];
index 249235b..4f247bf 100644 (file)
   "author": "Microsoft",
   "license": "MIT",
   "devDependencies": {
+    "@rollup/plugin-terser": "0.4.1",
     "@rollup/plugin-typescript": "11.1.0",
     "@rollup/plugin-virtual": "3.0.1",
-    "@rollup/plugin-terser": "0.4.1",
     "@typescript-eslint/eslint-plugin": "5.59.1",
     "@typescript-eslint/parser": "5.59.1",
+    "magic-string": "0.30.0",
     "eslint": "8.39.0",
     "fast-glob": "3.2.12",
     "git-commit-info": "2.0.1",
index 9d10744..dd4b704 100644 (file)
@@ -10,9 +10,11 @@ import dts from "rollup-plugin-dts";
 import { createFilter } from "@rollup/pluginutils";
 import fast_glob from "fast-glob";
 import gitCommitInfo from "git-commit-info";
+import MagicString from "magic-string";
 
 const configuration = process.env.Configuration;
 const isDebug = configuration !== "Release";
+const isContinuousIntegrationBuild = process.env.ContinuousIntegrationBuild === "true" ? true : false;
 const productVersion = process.env.ProductVersion || "8.0.0-dev";
 const nativeBinDir = process.env.NativeBinDir ? process.env.NativeBinDir.replace(/"/g, "") : "bin";
 const monoWasmThreads = process.env.MonoWasmThreads === "true" ? true : false;
@@ -38,13 +40,15 @@ const banner_dts = banner + "//!\n//! This is generated file, see src/mono/wasm/
 // emcc doesn't know how to load ES6 module, that's why we need the whole rollup.js
 const inlineAssert = [
     {
-        pattern: /mono_assert\(([^,]*), *"([^"]*)"\);/gm,
         // eslint-disable-next-line quotes
-        replacement: 'if (!($1)) throw new Error("Assert failed: $2"); // inlined mono_assert'
+        pattern: 'mono_assert\\(([^,]*), *"([^"]*)"\\);',
+        // eslint-disable-next-line quotes
+        replacement: (match) => `if (!(${match[1]})) throw new Error("Assert failed: ${match[2]}"); // inlined mono_assert`
     },
     {
-        pattern: /mono_assert\(([^,]*), \(\) => *`([^`]*)`\);/gm,
-        replacement: "if (!($1)) throw new Error(`Assert failed: $2`); // inlined mono_assert"
+        // eslint-disable-next-line quotes
+        pattern: 'mono_assert\\(([^,]*), \\(\\) => *`([^`]*)`\\);',
+        replacement: (match) => `if (!(${match[1]})) throw new Error(\`Assert failed: ${match[2]}\`); // inlined mono_assert`
     }
 ];
 const checkAssert =
@@ -78,8 +82,28 @@ const envConstants = {
     monoDiagnosticsMock,
     gitHash,
     wasmEnableLegacyJsInterop,
+    isContinuousIntegrationBuild,
 };
 
+const locationCache = {};
+function sourcemapPathTransform(relativeSourcePath, sourcemapPath) {
+    let res = locationCache[relativeSourcePath];
+    if (res === undefined) {
+        if (!isContinuousIntegrationBuild) {
+            const sourcePath = path.resolve(
+                path.dirname(sourcemapPath),
+                relativeSourcePath
+            );
+            res = `file:///${sourcePath.replace(/\\/g, "/")}`;
+        } else {
+            relativeSourcePath = relativeSourcePath.substring(12);
+            res = `https://raw.githubusercontent.com/dotnet/runtime/${gitHash}/${relativeSourcePath}`;
+        }
+        locationCache[relativeSourcePath] = res;
+    }
+    return res;
+}
+
 function consts(dict) {
     // implement rollup-plugin-const in terms of @rollup/plugin-virtual
     // It's basically the same thing except "consts" names all its modules with a "consts:" prefix,
@@ -103,7 +127,7 @@ const typescriptConfigOptions = {
 };
 
 const outputCodePlugins = [consts(envConstants), typescript(typescriptConfigOptions)];
-const externalDependencies = ["module"];
+const externalDependencies = ["module", "process"];
 
 const loaderConfig = {
     treeshake: !isDebug,
@@ -114,25 +138,14 @@ const loaderConfig = {
             file: nativeBinDir + "/dotnet.js",
             banner,
             plugins,
+            sourcemap: true,
+            sourcemapPathTransform,
         }
     ],
     external: externalDependencies,
     plugins: [regexReplace(inlineAssert), regexCheck([checkAssert, checkNoRuntime]), ...outputCodePlugins],
     onwarn: onwarn
 };
-const typesConfig = {
-    input: "./types/export-types.ts",
-    output: [
-        {
-            format: "es",
-            file: nativeBinDir + "/dotnet.d.ts",
-            banner: banner_dts,
-            plugins: [writeOnChangePlugin()],
-        }
-    ],
-    external: externalDependencies,
-    plugins: [dts()],
-};
 const runtimeConfig = {
     treeshake: !isDebug,
     input: "exports.ts",
@@ -142,13 +155,28 @@ const runtimeConfig = {
             file: nativeBinDir + "/dotnet.runtime.js",
             banner,
             plugins,
+            sourcemap: true,
+            sourcemapPathTransform,
         }
     ],
     external: externalDependencies,
     plugins: [regexReplace(inlineAssert), regexCheck([checkAssert, checkNoLoader]), ...outputCodePlugins],
     onwarn: onwarn
 };
-const legacyConfig = {
+const typesConfig = {
+    input: "./types/export-types.ts",
+    output: [
+        {
+            format: "es",
+            file: nativeBinDir + "/dotnet.d.ts",
+            banner: banner_dts,
+            plugins: [writeOnChangePlugin()],
+        }
+    ],
+    external: externalDependencies,
+    plugins: [dts()],
+};
+const legacyTypesConfig = {
     input: "./net6-legacy/export-types.ts",
     output: [
         {
@@ -174,7 +202,7 @@ if (isDebug) {
         banner: banner_dts,
         plugins: [alwaysLF(), writeOnChangePlugin()],
     });
-    legacyConfig.output.push({
+    legacyTypesConfig.output.push({
         format: "es",
         file: "./dotnet-legacy.d.ts",
         banner: banner_dts,
@@ -221,7 +249,7 @@ const allConfigs = [
     loaderConfig,
     runtimeConfig,
     typesConfig,
-    legacyConfig,
+    legacyTypesConfig,
 ].concat(workerConfigs)
     .concat(diagnosticMockTypesConfig ? [diagnosticMockTypesConfig] : []);
 export default defineConfig(allConfigs);
@@ -336,19 +364,34 @@ function regexReplace(replacements = []) {
         }
     };
 
-    function executeReplacement(_, code) {
-        // TODO use MagicString for sourcemap support
-        let fixed = code;
-        for (const rep of replacements) {
-            const { pattern, replacement } = rep;
-            fixed = fixed.replace(pattern, replacement);
+    function executeReplacement(_, code, id) {
+        const magicString = new MagicString(code);
+        if (!codeHasReplacements(code, id, magicString)) {
+            return null;
         }
 
-        if (fixed == code) {
-            return null;
+        const result = { code: magicString.toString() };
+        result.map = magicString.generateMap({ hires: true });
+        return result;
+    }
+
+    function codeHasReplacements(code, id, magicString) {
+        let result = false;
+        let match;
+        for (const rep of replacements) {
+            const { pattern, replacement } = rep;
+            const rx = new RegExp(pattern, "gm");
+            while ((match = rx.exec(code))) {
+                result = true;
+                const updated = replacement(match);
+                const start = match.index;
+                const end = start + match[0].length;
+                magicString.overwrite(start, end, updated);
+            }
         }
 
-        return { code: fixed };
+        // eslint-disable-next-line no-cond-assign
+        return result;
     }
 }
 
index 4353fe7..c5a5693 100644 (file)
@@ -5,5 +5,6 @@
             "esnext",
             "dom"
         ],
+        "sourceMap": true,
     }
-}
+}
\ No newline at end of file
index c150a1f..2842fb1 100644 (file)
     </ItemGroup>
 
     <Copy SourceFiles="$(NativeBinDir)dotnet.js;
+                       $(NativeBinDir)dotnet.js.map;
                        $(NativeBinDir)dotnet.runtime.js;
+                       $(NativeBinDir)dotnet.runtime.js.map;
                        $(NativeBinDir)dotnet.native.js;
                        $(NativeBinDir)dotnet.d.ts;
                        $(NativeBinDir)dotnet-legacy.d.ts;
 
   <Target Name="SetMonoRollupEnvironment" DependsOnTargets="GetProductVersions">
     <PropertyGroup>
-      <MonoRollupEnvironment>Configuration:$(Configuration),NativeBinDir:$(NativeBinDir),ProductVersion:$(ProductVersion),MonoWasmThreads:$(MonoWasmThreads),DISABLE_LEGACY_JS_INTEROP:$(_DisableLegacyJsInterop),MonoDiagnosticsMock:$(MonoDiagnosticsMock)</MonoRollupEnvironment>
+      <MonoRollupEnvironment>Configuration:$(Configuration),NativeBinDir:$(NativeBinDir),ProductVersion:$(ProductVersion),MonoWasmThreads:$(MonoWasmThreads),DISABLE_LEGACY_JS_INTEROP:$(_DisableLegacyJsInterop),MonoDiagnosticsMock:$(MonoDiagnosticsMock),ContinuousIntegrationBuild:$(ContinuousIntegrationBuild)</MonoRollupEnvironment>
     </PropertyGroup>
 
     <PropertyGroup>
index e102f66..445f427 100644 (file)
@@ -32,6 +32,7 @@ public class AssetsComputingHelper
         bool copySymbols,
         string customIcuCandidateFilename,
         bool enableThreads,
+        bool emitSourceMap,
         out string reason)
     {
         var extension = candidate.GetMetadata("Extension");
@@ -55,6 +56,7 @@ public class AssetsComputingHelper
             ".dat" when !string.IsNullOrEmpty(customIcuCandidateFilename) && fileName != customIcuCandidateFilename => "custom icu file will be used instead of icu from the runtime pack",
             ".json" when fromMonoPackage && (fileName == "emcc-props" || fileName == "package") => $"{fileName}{extension} is not used by Blazor",
             ".ts" when fromMonoPackage && fileName == "dotnet.d" => "dotnet type definition is not used by Blazor",
+            ".map" when !emitSourceMap && fromMonoPackage && (fileName == "dotnet.js" || fileName == "dotnet.runtime.js") => "source map file is not published",
             ".ts" when fromMonoPackage && fileName == "dotnet-legacy.d" => "dotnet type definition is not used by Blazor",
             ".js" when assetType == "native" && !(dotnetJsSingleThreadNames.Contains(fileName) || (enableThreads && fileName == "dotnet.native.worker")) => $"{fileName}{extension} is not used by Blazor",
             ".pdb" when !copySymbols => "copying symbols is disabled",
index 580f528..ba25c51 100644 (file)
@@ -50,6 +50,8 @@ public class ComputeWasmBuildAssets : Task
 
     public bool EnableThreads { get; set; }
 
+    public bool EmitSourceMap { get; set; }
+
     [Output]
     public ITaskItem[] AssetCandidates { get; set; }
 
@@ -84,7 +86,7 @@ public class ComputeWasmBuildAssets : Task
             for (int i = 0; i < Candidates.Length; i++)
             {
                 var candidate = Candidates[i];
-                if (AssetsComputingHelper.ShouldFilterCandidate(candidate, TimeZoneSupport, InvariantGlobalization, CopySymbols, customIcuCandidateFilename, EnableThreads, out var reason))
+                if (AssetsComputingHelper.ShouldFilterCandidate(candidate, TimeZoneSupport, InvariantGlobalization, CopySymbols, customIcuCandidateFilename, EnableThreads, EmitSourceMap, out var reason))
                 {
                     Log.LogMessage(MessageImportance.Low, "Skipping asset '{0}' because '{1}'", candidate.ItemSpec, reason);
                     filesToRemove.Add(candidate);
index dd90754..8a9bcc5 100644 (file)
@@ -58,6 +58,8 @@ public class ComputeWasmPublishAssets : Task
 
     public bool EnableThreads { get; set; }
 
+    public bool EmitSourceMap { get; set; }
+
     public bool IsWebCilEnabled { get; set; }
 
     [Output]
@@ -575,7 +577,7 @@ public class ComputeWasmPublishAssets : Task
 
         foreach (var candidate in resolvedFilesToPublish)
         {
-            if (AssetsComputingHelper.ShouldFilterCandidate(candidate, TimeZoneSupport, InvariantGlobalization, CopySymbols, customIcuCandidateFilename, EnableThreads, out var reason))
+            if (AssetsComputingHelper.ShouldFilterCandidate(candidate, TimeZoneSupport, InvariantGlobalization, CopySymbols, customIcuCandidateFilename, EnableThreads, EmitSourceMap, out var reason))
             {
                 Log.LogMessage(MessageImportance.Low, "Skipping asset '{0}' because '{1}'", candidate.ItemSpec, reason);
                 if (!resolvedFilesToPublishToRemove.ContainsKey(candidate.ItemSpec))
index 88cf5fa..12e7cb3 100644 (file)
@@ -150,6 +150,12 @@ public class WasmAppBuilder : WasmAppBuilderBaseTask
             if (!IncludeThreadsWorker && name == "dotnet.native.worker.js")
                 continue;
 
+            if (name == "dotnet.runtime.js.map" || name == "dotnet.js.map")
+            {
+                Log.LogMessage(MessageImportance.Low, $"Skipping {item.ItemSpec} from boot config");
+                continue;
+            }
+
             var itemHash = Utils.ComputeIntegrity(item.ItemSpec);
 
             if (name.StartsWith("dotnet", StringComparison.OrdinalIgnoreCase) && string.Equals(Path.GetExtension(name), ".wasm", StringComparison.OrdinalIgnoreCase))