diff --git a/ReferenceClientProxyImplementation/Patches/Constants/Constant.cs b/ReferenceClientProxyImplementation/Patches/Constants/Constant.cs
new file mode 100644
index 0000000..09a00cc
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Constants/Constant.cs
@@ -0,0 +1,6 @@
+namespace ReferenceClientProxyImplementation.Patches.Constants;
+
+public class Constant {
+ public required string SourceValue { get; set; }
+ public required string TargetValue { get; set; }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/IPatch.cs b/ReferenceClientProxyImplementation/Patches/IPatch.cs
new file mode 100644
index 0000000..92cab93
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/IPatch.cs
@@ -0,0 +1,8 @@
+namespace ReferenceClientProxyImplementation.Patches;
+
+public interface IPatch {
+ public int GetOrder();
+ public string GetName();
+ public bool Applies(string relativeName, byte[] content);
+ public Task<byte[]> Execute(string relativeName, byte[] content);
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs
new file mode 100644
index 0000000..649625f
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/FormatFilePatch.cs
@@ -0,0 +1,91 @@
+using System.Diagnostics;
+using ArcaneLibs;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations;
+
+public partial class FormatJsFilePatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => -100;
+
+ public string GetName() => "Format JS file";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js") || relativeName.EndsWith(".css") || relativeName.EndsWith(".html");
+
+ public async Task<byte[]> Execute(string relativeName, byte[] content) {
+ var cachePath = Path.Combine(config.TestClient.RevisionPath, "formatted", relativeName);
+ if (File.Exists(cachePath)) {
+ Console.WriteLine($"Using cached formatted file for {relativeName}");
+ return await File.ReadAllBytesAsync(cachePath);
+ }
+
+ // temporary: add some newlines
+ var stringContent = System.Text.Encoding.UTF8.GetString(content);
+ // stringContent = stringContent.Replace("function(){", "function() {\n");
+ content = System.Text.Encoding.UTF8.GetBytes(stringContent);
+
+
+ Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
+ var tmpPath = $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{Random.Shared.NextInt64()}_{Path.GetFileName(relativeName)}";
+ await File.WriteAllBytesAsync(tmpPath, content);
+ var sw = Stopwatch.StartNew();
+ ProcessStartInfo psi;
+
+ // Biome doesn't support HTML and struggles with upstream emitting Sass directives
+ if (relativeName.EndsWith(".html") || relativeName.EndsWith(".css"))
+ psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+ else
+ psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") {
+ // psi = new ProcessStartInfo(config.AssetCache.BiomePath, $"format --write --line-width 240 --files-max-size=100000000 {tmpPath}") {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(psi);
+ if (process == null) {
+ throw new InvalidOperationException("Failed to start the formatting process.");
+ }
+
+ // var stdout = await process.StandardOutput.ReadToEndAsync();
+ // var stderr = await process.StandardError.ReadToEndAsync();
+ // await process.WaitForExitAsync();
+
+ Dictionary<ulong, string> stdoutLines = new();
+ Dictionary<ulong, string> stderrLines = new();
+
+ process.OutputDataReceived += (sender, args) => {
+ if (args.Data != null) {
+ stdoutLines[(ulong)sw.ElapsedMilliseconds] = args.Data;
+ Console.Write("O");
+ }
+ };
+ process.ErrorDataReceived += (sender, args) => {
+ if (args.Data != null) {
+ stderrLines[(ulong)sw.ElapsedMilliseconds] = args.Data;
+ Console.Write("E");
+ }
+ };
+ process.BeginOutputReadLine();
+ process.BeginErrorReadLine();
+
+ await process.WaitForExitAsync();
+
+ Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+
+ if (process.ExitCode != 0) {
+ Console.WriteLine($"Failed to format {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+ Console.WriteLine("Standard Output:\n" + string.Join("\n", stdoutLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}")));
+ Console.WriteLine("Standard Error:\n" + string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}")));
+ throw new Exception($"Failed to exec({psi.FileName} {psi.Arguments}): {string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => kv.Value))}");
+ }
+
+ var result = await File.ReadAllBytesAsync(tmpPath);
+ File.Move(tmpPath, cachePath);
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs
new file mode 100644
index 0000000..877a31a
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/FormatHtmlCssPatch.cs
@@ -0,0 +1,72 @@
+using System.Diagnostics;
+using ArcaneLibs;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations;
+
+public partial class FormatHtmlCssPatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => -100;
+
+ public string GetName() => "Format HTML/CSS file";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".css") || relativeName.EndsWith(".html");
+
+ public async Task<byte[]> Execute(string relativeName, byte[] content) {
+ var cachePath = Path.Combine(config.TestClient.RevisionPath, "formatted", relativeName);
+ if (File.Exists(cachePath)) {
+ Console.WriteLine($"Using cached formatted file for {relativeName}");
+ return await File.ReadAllBytesAsync(cachePath);
+ }
+
+ Directory.CreateDirectory(Path.GetDirectoryName(cachePath)!);
+ var tmpPath = $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{Random.Shared.NextInt64()}_{Path.GetFileName(relativeName)}";
+ await File.WriteAllBytesAsync(tmpPath, content);
+ var sw = Stopwatch.StartNew();
+ ProcessStartInfo psi = new ProcessStartInfo(config.AssetCache.PrettierPath, $"-w --print-width 240 {tmpPath}") {
+ RedirectStandardOutput = true,
+ RedirectStandardError = true,
+ UseShellExecute = false,
+ CreateNoWindow = true
+ };
+
+ using var process = Process.Start(psi);
+ if (process == null) {
+ throw new InvalidOperationException("Failed to start the formatting process.");
+ }
+
+ // var stdout = await process.StandardOutput.ReadToEndAsync();
+ // var stderr = await process.StandardError.ReadToEndAsync();
+ // await process.WaitForExitAsync();
+
+ Dictionary<ulong, string> stdoutLines = new();
+ Dictionary<ulong, string> stderrLines = new();
+
+ while (!process.HasExited) {
+ while (!process.StandardOutput.EndOfStream) {
+ var line = await process.StandardOutput.ReadLineAsync();
+ if (line == null) continue;
+ stdoutLines[(ulong)sw.ElapsedMilliseconds] = line;
+ Console.Write("O");
+ }
+
+ while (!process.StandardError.EndOfStream) {
+ var line = await process.StandardError.ReadLineAsync();
+ if (line == null) continue;
+ stderrLines[(ulong)sw.ElapsedMilliseconds] = line;
+ Console.Write("E");
+ }
+ }
+
+ // Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+
+ if (process.ExitCode != 0) {
+ Console.WriteLine($"Failed to format {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+ Console.WriteLine("Standard Output:\n" + string.Join("\n", stdoutLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}")));
+ Console.WriteLine("Standard Error:\n" + string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => $"[{kv.Key}ms] {kv.Value}")));
+ throw new Exception($"Failed to format file {relativeName}: {string.Join("\n", stderrLines.OrderBy(kv => kv.Key).Select(kv => kv.Value))}");
+ }
+
+ var result = await File.ReadAllBytesAsync(tmpPath);
+ File.Move(tmpPath, cachePath);
+ return result;
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs
new file mode 100644
index 0000000..dc54850
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/GlobalEnvPatch.cs
@@ -0,0 +1,22 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.HTMLPatches;
+
+public class GlobalEnvPatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Patch GLOBAL_ENV";
+ public bool Applies(string relativeName, byte[] content) => relativeName is "app.html" or "developers.html" or "popout.html";
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ foreach(var (key, value) in config.TestClient.GlobalEnv) {
+ stringContent = new Regex($"{key}: \".*?\"").Replace(stringContent, $"{key}: \"{value}\"");
+ }
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs
new file mode 100644
index 0000000..6c5f312
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/HTMLPatches/StripNoncesPatch.cs
@@ -0,0 +1,36 @@
+using System.Text;
+using System.Text.RegularExpressions;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.HTMLPatches;
+
+public partial class StripNoncesPatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => "Strip nonces/integrity from html";
+ public bool Applies(string relativeName, byte[] content) => relativeName is "app.html" or "developers.html" or "popout.html";
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+ stringContent = HtmlScriptIntegrityRegex().Replace(
+ HtmlScriptNonceRegex().Replace(
+ JsElementNonceRegex().Replace(
+ stringContent,
+ ""
+ ),
+ ""
+ ),
+ ""
+ );
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex("\\snonce=\"[a-zA-Z0-9+/=]+\"")]
+ private static partial Regex HtmlScriptNonceRegex();
+
+ [GeneratedRegex("\\w.nonce='[a-zA-Z0-9+/=]+';")]
+ private static partial Regex JsElementNonceRegex();
+
+ [GeneratedRegex(@"\sintegrity=""[a-zA-Z0-9+/=\-\s]+""")]
+ private static partial Regex HtmlScriptIntegrityRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs
new file mode 100644
index 0000000..6e7f7a2
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ApiProtocolPatch.cs
@@ -0,0 +1,32 @@
+using System.Text;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public class ApiProtocolPatch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => "API: Use GLOBAL_ENV.API_PROTOCOL instead of hardcoded https:";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ // TODO: regex
+ stringContent = stringContent
+ .Replace(
+ "return \"https:\" + window.GLOBAL_ENV.API_ENDPOINT + (e ? \"/v\".concat(window.GLOBAL_ENV.API_VERSION) : \"\");",
+ "return window.GLOBAL_ENV.API_PROTOCOL + window.GLOBAL_ENV.API_ENDPOINT + (e ? \"/v\".concat(window.GLOBAL_ENV.API_VERSION) : \"\");"
+ )
+ .Replace(
+ "api_endpoint: \"\".concat(\"https:\").concat(window.GLOBAL_ENV.API_ENDPOINT)",
+ "api_endpoint: window.GLOBAL_ENV.API_PROTOCOL.concat(window.GLOBAL_ENV.API_ENDPOINT)"
+ )
+ .Replace(
+ "f = null != d ? \"https://\".concat(d) : location.protocol + window.GLOBAL_ENV.API_ENDPOINT,",
+ "f = null != d ? window.GLOBAL_ENV.API_PROTOCOL.concat(d) : location.protocol + window.GLOBAL_ENV.API_ENDPOINT,"
+ )
+ ;
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs
new file mode 100644
index 0000000..152a3f3
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPatch.cs
@@ -0,0 +1,21 @@
+using System.Text;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public class BooleanPatch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => "Use real booleans in JS files";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = stringContent
+ .Replace("return!", "return !")
+ .Replace("!0", "true")
+ .Replace("!1", "false");
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs
new file mode 100644
index 0000000..fe73c8e
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/BooleanPropagationPatch.cs
@@ -0,0 +1,21 @@
+// using System.Text;
+// using System.Text.RegularExpressions;
+//
+// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+//
+// public partial class BooleanPropagationPatch : IPatch {
+// public int GetOrder() => 3;
+//
+// public string GetName() => "Patch pointless boolean comparisons in JS";
+// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+//
+// public async Task<byte[]> Execute(string relativePath, byte[] content) {
+// var stringContent = Encoding.UTF8.GetString(content);
+//
+// stringContent = stringContent
+// .Replace(" && true", "").Replace(" || false", "").Replace("false || ", "")
+// ;
+//
+// return Encoding.UTF8.GetBytes(stringContent);
+// }
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs
new file mode 100644
index 0000000..c44bf95
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/DisableSciencePatch.cs
@@ -0,0 +1,26 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class DisableSciencePatch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => @"JS(web): Disable /science calls";
+ public bool Applies(string relativeName, byte[] content) => relativeName.StartsWith("assets/web.") && relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ var match = HandleTrackDefinitionRegex().Match(stringContent);
+ stringContent = stringContent.Insert(match.Index + match.Length, @"
+ return (new Promise(() => { }), false); // ReferenceClientProxyImplementation: Disable /science calls
+ ");
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@".\.handleTrack = function \(.\) \{", RegexOptions.Compiled)]
+ private static partial Regex HandleTrackDefinitionRegex();
+
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs
new file mode 100644
index 0000000..9eed00c
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/ExpandUnicodeEscapesPatch.cs
@@ -0,0 +1,26 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class ExpandUnicodeEscapesPatch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => @"JS: expand \x?? to \u00??";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = XToURegex().Replace(
+ stringContent,
+ m => $"\\u00{m.Groups[1].Value}"
+ );
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@"\\x([0-9A-Fa-f]{2})", RegexOptions.Compiled)]
+ private static partial Regex XToURegex();
+
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs
new file mode 100644
index 0000000..b741f56
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/IsStaffPatch.cs
@@ -0,0 +1,33 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class IsStaffPatch : IPatch {
+ public int GetOrder() => 2;
+
+ public string GetName() => "Patch isStaff/isStaffPersonal in JS";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string relativePath, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = IsNullableStaffRegex().Replace(
+ stringContent,
+ m => $"{m.Groups[1].Value}!!{m.Groups[2].Value}"
+ );
+
+ stringContent = IsStaffRegex().Replace(
+ stringContent,
+ m => $"{m.Groups[1].Value}true"
+ );
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@"(\W)(\w|this|\w\.user)\.isStaff(Personal)?\(\)", RegexOptions.Compiled)]
+ private static partial Regex IsStaffRegex();
+
+ [GeneratedRegex(@"(\W)(\w|this|\w\.user)\?\.isStaff(Personal)?\(\)", RegexOptions.Compiled)]
+ private static partial Regex IsNullableStaffRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs
new file mode 100644
index 0000000..b5e7d77
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/JsonParseMultilinePatch.cs
@@ -0,0 +1,79 @@
+using System.Diagnostics;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using ReferenceClientProxyImplementation.Configuration;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class JsonParseMultilinePatch(ProxyConfiguration config) : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Patch null-coalescing expressions in JS";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string relativePath, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ var matches = JsonParseRegex().Matches(stringContent);
+ Console.WriteLine($"Found {matches.Count} JSON.parse calls in {relativePath}");
+
+
+
+ await Parallel.ForEachAsync(matches, async (match, ct) => {
+ string formattedJson = match.Groups[1].Value;
+ try {
+ var jsonElement = JsonSerializer.Deserialize<JsonElement>(formattedJson.Replace("\\", "\\\\") + "waef");
+ formattedJson = JsonSerializer.Serialize(jsonElement, new JsonSerializerOptions { WriteIndented = true });
+ } catch (JsonException je) {
+ // Console.WriteLine($"STJ: Failed to parse JSON in {relativePath} at index {match.Index}: {je.Message}"); // intentinally broken
+ try {
+ formattedJson = await formatJsonWithNodejs(relativePath, match, ct);
+ } catch (Exception e) {
+ Console.WriteLine($"Node.js: Failed to parse JSON in {relativePath} at index {match.Index}: {e.Message}");
+ return;
+ }
+ }
+
+ lock (matches) stringContent = stringContent.Replace(match.Value, $"JSON.parse(`{formattedJson.Replace("\\", "\\\\")}`);");
+ });
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ private async Task<string> formatJsonWithNodejs(string relativePath, Match match, CancellationToken ct) {
+ // Extract the JSON string from the match
+ var id = "dcp_" + Path.GetFileName(relativePath).Replace('.', '_') + "_" + match.Index;
+ await File.WriteAllTextAsync($"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js", $"console.log(JSON.stringify(JSON.parse(`{match.Groups[1].Value.Replace("`", "\\\\\\`")}`), null, 2))");
+ var sw = Stopwatch.StartNew();
+
+ var psi = new ProcessStartInfo(config.AssetCache.NodePath, $"{Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js") { RedirectStandardOutput = true, RedirectStandardError = true, UseShellExecute = false, CreateNoWindow = true };
+
+ using var process = Process.Start(psi);
+ if (process == null) {
+ throw new InvalidOperationException("Failed to start the formatting process.");
+ }
+
+ var stdout = await process.StandardOutput.ReadToEndAsync();
+ var stderr = await process.StandardError.ReadToEndAsync();
+
+ await process.WaitForExitAsync();
+ // Console.WriteLine($"Formatted {relativeName} in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+
+ if (process.ExitCode != 0) {
+ Console.WriteLine($"Failed to run {Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js in {sw.ElapsedMilliseconds}ms: {process.ExitCode}");
+ Console.WriteLine("Standard Output: " + stdout);
+ Console.WriteLine("Standard Error: " + stderr);
+ throw new Exception($"Failed to execute {Environment.GetEnvironmentVariable("TMPDIR") ?? "/tmp"}/{id}.js: {stderr}");
+ }
+
+ var formattedJson = stdout.Trim();
+ Console.WriteLine($"Parsed JSON({id}) in {sw.ElapsedMilliseconds}ms: {formattedJson.Length} bytes");
+ // stringContent = stringContent.Replace(match.Value, $"JSON.parse(`{formattedJson.Replace("\\n", "\\\\n")}`);");
+ await File.WriteAllTextAsync($"{config.TestClient.RevisionPath}/patched/assets/{Path.GetFileName(relativePath)}-{match.Index}.json", formattedJson);
+ return formattedJson;
+ }
+
+ [GeneratedRegex(@"JSON\.parse\(\n\s*'(.*?)',?\s*\);", RegexOptions.Compiled | RegexOptions.Multiline)]
+ private static partial Regex JsonParseRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs
new file mode 100644
index 0000000..a94e312
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/KnownConstantsPatch.cs
@@ -0,0 +1,16 @@
+using System.Text;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public class KnownConstantsPatch : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Use named constants";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs
new file mode 100644
index 0000000..e7f78a0
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LegacyJsPatches.cs
@@ -0,0 +1,32 @@
+// using System.Text;
+// using System.Text.RegularExpressions;
+//
+// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+//
+// public partial class LegacyJsPathces : IPatch {
+// public int GetOrder() => 1;
+//
+// public string GetName() => "Patch deprecated JS constructs";
+// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+//
+// public async Task<byte[]> Execute(string relativePath, byte[] content) {
+// var stringContent = Encoding.UTF8.GetString(content);
+//
+// while(MozInputSourceRegex().IsMatch(stringContent)) {
+// var match = MozInputSourceRegex().Match(stringContent);
+// var replacement = match.Groups[1].Value switch {
+// "0" => "",
+// "1" => "mouse",
+// "2" => "pen",
+// "3" => "pen",
+// "4" => "touch",
+// _ => throw new InvalidOperationException("Unreachable")
+// };
+// }
+//
+// return Encoding.UTF8.GetBytes(stringContent);
+// }
+//
+// [GeneratedRegex(@"([0-6]) === (\w).mozInputSource", RegexOptions.Compiled)]
+// private static partial Regex MozInputSourceRegex();
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs
new file mode 100644
index 0000000..2005c4c
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/LogErrorContextPatch.cs
@@ -0,0 +1,34 @@
+// using System.Text;
+// using System.Text.RegularExpressions;
+//
+// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+//
+// public partial class LogErrorContextPatch : IPatch {
+// public int GetOrder() => 2;
+//
+// public string GetName() => "Patch assertions to log more context";
+// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+//
+// public async Task<byte[]> Execute(string relativePath, byte[] content) {
+// var stringContent = Encoding.UTF8.GetString(content);
+//
+// stringContent = NotNullAssertionRegex().Replace(
+// stringContent,
+// m => {
+// var methodName = m.Groups[1].Value;
+// var objectName = m.Groups[2].Value;
+// var message = m.Groups[3].Value;
+// Console.WriteLine($"Patching not-null assertion in {relativePath}: {methodName} - {message}");
+//
+// return $@"{methodName}()(null != {objectName}, ""{message} - Context: "" + JSON.stringify({objectName}))";
+// }
+// );
+//
+// return Encoding.UTF8.GetBytes(stringContent);
+// }
+//
+// // null assertion: u()(null != o, "PrivateChannel.renderAvatar: Invalid prop configuration - no user or channel");
+// // capture: method name, object name, message
+// [GeneratedRegex(@"([a-zA-Z0-9_]+)\(\)\(\s*null != ([a-zA-Z0-9_]+),\s*""([^""]+)""\s*\)", RegexOptions.Compiled)]
+// private static partial Regex NotNullAssertionRegex();
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs
new file mode 100644
index 0000000..98156c8
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/NullCoalescingPatch.cs
@@ -0,0 +1,32 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class NullCoalescingPatch : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Patch null-coalescing expressions in JS";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string relativePath, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = NullCoalescingRegex().Replace(
+ stringContent,
+ m => $"{m.Groups[1].Value}?.{m.Groups[2].Value}"
+ );
+ // stringContent = ParenNullCheckRegex().Replace(
+ // stringContent,
+ // m => $"{m.Groups[1].Value} == null"
+ // );
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@"null == ([a-zA-Z0-9_]+?) \? undefined : \1\.([a-zA-Z0-9_]+?)", RegexOptions.Compiled)]
+ private static partial Regex NullCoalescingRegex();
+
+ [GeneratedRegex(@"\(([^()]+?)\) == null", RegexOptions.Compiled)]
+ private static partial Regex ParenNullCheckRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs
new file mode 100644
index 0000000..bab0756
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/PrefetchAssetsPatch.cs
@@ -0,0 +1,56 @@
+// using System.Text;
+// using System.Text.RegularExpressions;
+// using ReferenceClientProxyImplementation.Services;
+//
+// namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+//
+// public partial class PrefetchAssetsPatch(IServiceProvider sp) : IPatch {
+// public int GetOrder() => 1000000;
+//
+// public string GetName() => "Prefetch assets";
+// public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+//
+// private static SemaphoreSlim ss = new(2, 2);
+// private static HashSet<string> alreadyKnownAssets = new();
+//
+// public async Task<byte[]> Execute(string relativePath, byte[] content) {
+// // Can't inject service due to loop
+// var stringContent = Encoding.UTF8.GetString(content);
+// var matches = PrefetchAssetsRegex().Matches(stringContent);
+//
+// Console.WriteLine($"Found {matches.Count} prefetch assets in {relativePath}");
+// if (matches.Count == 0) {
+// return content; // No matches found, return original content
+// }
+//
+// var clientStore = sp.GetRequiredService<ClientStoreService>();
+//
+// var newAssets = matches
+// .Select(x => x.Groups[1].Value)
+// .Distinct()
+// .Where(x => !clientStore.HasRawAsset(x) && alreadyKnownAssets.Add(x));
+//
+// var tasks = newAssets
+// .Select(async match => {
+// await ss.WaitAsync();
+// Console.WriteLine($"Discovered prefetch asset in {relativePath}: {match}");
+// // var patches = sp.GetRequiredService<PatchSet>();
+// var res = await clientStore.GetOrDownloadRawAsset(match);
+// await res.DisposeAsync();
+// ss.Release();
+// Console.WriteLine($"Prefetched asset {match} in {relativePath}");
+// }).ToList();
+//
+// if (tasks.Count == 0) {
+// Console.WriteLine($"No new prefetch assets found in {relativePath}, returning original content.");
+// return content; // No new assets to prefetch, return original content
+// }
+//
+// await Task.WhenAny(tasks);
+//
+// return content;
+// }
+//
+// [GeneratedRegex(@".\.exports = ""((?:[a-z\d/\.]*?)\.\w{2,4})""", RegexOptions.Compiled)]
+// private static partial Regex PrefetchAssetsRegex();
+// }
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs
new file mode 100644
index 0000000..9d819c7
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/Void0Patch.cs
@@ -0,0 +1,27 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class Void0Patch : IPatch {
+ public int GetOrder() => 0;
+
+ public string GetName() => "Use literal undefined instead of void 0";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string _, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = stringContent
+ .Replace("void 0", "undefined");
+ stringContent = VoidFunctionRegex().Replace(
+ stringContent,
+ m => $"{m.Groups[1].Value}("
+ );
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+
+ [GeneratedRegex(@"\(0, ([a-zA-Z0-9_.$]+?)\)\(", RegexOptions.Compiled)]
+ private static partial Regex VoidFunctionRegex();
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs
new file mode 100644
index 0000000..277bf8a
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/JSPatches/WhileTruePatch.cs
@@ -0,0 +1,19 @@
+using System.Text;
+using System.Text.RegularExpressions;
+
+namespace ReferenceClientProxyImplementation.Patches.Implementations.JSPatches;
+
+public partial class WhileTruePatch : IPatch {
+ public int GetOrder() => 1;
+
+ public string GetName() => "Patch while(true) expressions in JS";
+ public bool Applies(string relativeName, byte[] content) => relativeName.EndsWith(".js");
+
+ public async Task<byte[]> Execute(string relativePath, byte[] content) {
+ var stringContent = Encoding.UTF8.GetString(content);
+
+ stringContent = stringContent.Replace("for (;;)", "while (true)");
+
+ return Encoding.UTF8.GetBytes(stringContent);
+ }
+}
\ No newline at end of file
diff --git a/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs b/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs
new file mode 100644
index 0000000..c3dba59
--- /dev/null
+++ b/ReferenceClientProxyImplementation/Patches/Implementations/PatchSet.cs
@@ -0,0 +1,26 @@
+namespace ReferenceClientProxyImplementation.Patches.Implementations;
+
+public class PatchSet(IServiceProvider sp) {
+ public List<IPatch> Patches { get; } = sp.GetServices<IPatch>().OrderBy(x => x.GetOrder()).ToList();
+
+ public async Task<byte[]> ApplyPatches(string relativeName, byte[] content) {
+ var i = 0;
+ var patches = Patches
+ .Where(p => p.Applies(relativeName, content))
+ .OrderBy(p => p.GetOrder())
+ .ToList();
+ foreach (var patch in patches) {
+ if (patch.Applies(relativeName, content)) {
+ var defaultColor = Console.ForegroundColor;
+ Console.ForegroundColor = ConsoleColor.DarkBlue;
+ Console.Write("==> ");
+ Console.ForegroundColor = ConsoleColor.DarkGray;
+ Console.WriteLine($"Running task {++i}/{patches.Count}: {patch.GetName()} (Type<{patch.GetType().Name}>)");
+ Console.ForegroundColor = defaultColor;
+ content = await patch.Execute(relativeName, content);
+ }
+ }
+
+ return content;
+ }
+}
\ No newline at end of file
|