Add wasm to render console in frontend#163
Conversation
There was a problem hiding this comment.
Pull request overview
This PR adds WebAssembly support to render console output in a frontend application. The implementation introduces a new Blazor WebAssembly project (RazorConsole.Website) with xterm.js integration for terminal rendering in the browser.
Key changes:
- New Blazor WebAssembly project setup
- Integration of xterm.js library for terminal emulation
- JavaScript interop layer for terminal operations
- Static assets (Bootstrap, fonts, icons, sample data)
Reviewed changes
Copilot reviewed 31 out of 76 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| RazorConsole.Website.csproj | New Blazor WebAssembly project targeting .NET 10.0 |
| Properties/launchSettings.json | Development server configuration with HTTP/HTTPS profiles |
| wwwroot/index.html | Main HTML entry point with xterm.js and Blazor bootstrap |
| wwwroot/js/xterm-interop.js | JavaScript interop for terminal initialization and I/O |
| wwwroot/lib/* | Third-party libraries (xterm.js, Bootstrap CSS) |
| wwwroot/sample-data/weather.json | Sample weather data fixture |
| wwwroot/*.png | Application icons and favicon |
| _Imports.razor | Global Razor component imports |
| Pages/NotFound.razor | 404 error page component |
| </div> | ||
| <script src="lib/xterm.min.js"></script> | ||
| <script src="js/xterm-interop.js"></script> | ||
| <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script> |
There was a problem hiding this comment.
The Blazor WebAssembly script URL contains invalid syntax. The fingerprint placeholder format #[.{fingerprint}] is non-standard. It should be either blazor.webassembly.js for development or use the correct MSBuild token syntax like _framework/blazor.webassembly.js (the framework handles fingerprinting automatically during publish).
| <script src="_framework/blazor.webassembly#[.{fingerprint}].js"></script> | |
| <script src="_framework/blazor.webassembly.js"></script> |
|
|
||
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <TargetFrameworks></TargetFrameworks> |
There was a problem hiding this comment.
The empty <TargetFrameworks> element should be removed as it serves no purpose and may cause confusion. The singular <TargetFramework> on line 4 is already specified and sufficient.
| <TargetFrameworks></TargetFrameworks> |
| const terminal = getTerminal(elementId); | ||
|
|
||
| terminal.onKey(function (event) { | ||
| dotnetHelper.invokeMethodAsync("HandleKeyboardEvent", event.key, event.domEvent.key, event.domEvent.ctrlKey, event.domEvent.altKey, event.domEvent.shiftKey); |
There was a problem hiding this comment.
The event.key parameter is passed twice (once as the first parameter and again as event.domEvent.key). This appears redundant - verify if both are needed or remove the duplicate.
| dotnetHelper.invokeMethodAsync("HandleKeyboardEvent", event.key, event.domEvent.key, event.domEvent.ctrlKey, event.domEvent.altKey, event.domEvent.shiftKey); | |
| dotnetHelper.invokeMethodAsync("HandleKeyboardEvent", event.key, event.domEvent.ctrlKey, event.domEvent.altKey, event.domEvent.shiftKey); |
| <link rel="stylesheet" href="css/app.css" /> | ||
| <link rel="icon" type="image/png" href="favicon.png" /> | ||
| <link href="RazorConsole.Website.styles.css" rel="stylesheet" /> | ||
| <script type="importmap"></script> |
There was a problem hiding this comment.
Empty import map script tag should either be removed or populated with actual import mappings. An empty import map serves no purpose and adds unnecessary DOM elements.
| <script type="importmap"></script> |
| { | ||
| throw new InvalidOperationException("The renderer has not been initialized."); | ||
| } | ||
| var sw = new StringWriter(); |
There was a problem hiding this comment.
Disposable 'StringWriter' is created but not disposed.
| var sw = new StringWriter(); | |
| using var sw = new StringWriter(); |
| console.Profile.Width = 80; | ||
| console.Profile.Height = 150; | ||
|
|
||
| var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150)); |
There was a problem hiding this comment.
This assignment to consoleOption is useless, since its value is never read.
| var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150)); |
| var modifiers = ConsoleModifiers.None; | ||
| if (ctrlKey) | ||
| { | ||
| modifiers |= ConsoleModifiers.Control; | ||
| } | ||
| if (altKey) | ||
| { | ||
| modifiers |= ConsoleModifiers.Alt; | ||
| } | ||
| if (shiftKey) | ||
| { | ||
| modifiers |= ConsoleModifiers.Shift; | ||
| } |
There was a problem hiding this comment.
This assignment to modifiers is useless, since its value is never read.
| var modifiers = ConsoleModifiers.None; | |
| if (ctrlKey) | |
| { | |
| modifiers |= ConsoleModifiers.Control; | |
| } | |
| if (altKey) | |
| { | |
| modifiers |= ConsoleModifiers.Alt; | |
| } | |
| if (shiftKey) | |
| { | |
| modifiers |= ConsoleModifiers.Shift; | |
| } |
|
I was thinking about a bit diffrent approach.
Docs site is still using react and not blazor. We can export blazor components as react (i.e. builder.RootComponents.RegisterForJavaScript<Quote>(identifier: "quote",
javaScriptInitializer: "initializeComponent");) but im not sure how well it works. There is a I like how you using copilot. can i also ask it to make changes in draft? (and docs whould be nice) |
Looks very good, but maybe .net runtime in wasm will require some initial time in client, we must find the way to init runtime once in client, and then just run wasm examples.
You can use copilot in comments to drafts, just summoning him by |
…TermPreview component
…eview for better component visualization
… terminal interop
…ws for components
…es for better usability
| <PropertyGroup> | ||
| <TargetFramework>net10.0</TargetFramework> | ||
| <OutputType>exe</OutputType> | ||
| <TargetFrameworks></TargetFrameworks> |
There was a problem hiding this comment.
The TargetFrameworks element on line 6 is empty, which might cause build issues. This should either specify target frameworks or be removed entirely since TargetFramework is already defined on line 4.
| await blazorStartPromise | ||
| } | ||
|
|
||
| export async function mountComponentPreview(componentName: string, terminalElementId: string): Promise<() => void> { |
There was a problem hiding this comment.
[nitpick] The function returns () => void but the returned arrow function doesn't have an explicit return statement. While this works in JavaScript, it would be clearer to either type it as Promise<() => void> or document that the returned cleanup function is synchronous.
| console.Profile.Width = 80; | ||
| console.Profile.Height = 150; |
There was a problem hiding this comment.
Magic numbers detected. The terminal dimensions (80 columns, 150 rows) are hardcoded here as well. These values should be consistent with other parts of the application and ideally centralized in a shared configuration.
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.WriteLine($"Error rendering component: {ex.Message} {ex.StackTrace}"); |
There was a problem hiding this comment.
Console.WriteLine should not be used for error logging in production code. This error handling also re-throws the exception after logging, which is acceptable, but consider using ILogger for consistent logging throughout the application.
| console.log(`Getting existing terminal: ${elementId}`); | ||
| const terminal = getTerminalInstance(elementId); | ||
| console.log('Terminal found:', terminal); |
There was a problem hiding this comment.
Console.WriteLine calls should be removed or replaced with proper logging. These debug statements appear in multiple places throughout the interop code.
| { | ||
| throw new InvalidOperationException("The renderer has not been initialized."); | ||
| } | ||
| var sw = new StringWriter(); |
There was a problem hiding this comment.
Disposable 'StringWriter' is created but not disposed.
| console.Profile.Width = 80; | ||
| console.Profile.Height = 150; | ||
|
|
||
| var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150)); |
There was a problem hiding this comment.
This assignment to consoleOption is useless, since its value is never read.
| <Markup Content="Use arrow keys to navigate and Enter to select." Foreground="Color.Green" /> | ||
| </Rows> | ||
| @code { | ||
| private string[] options = new[] { "Option 1", "Option 2", "Option 3" }; |
There was a problem hiding this comment.
Field 'options' can be 'readonly'.
| private string[] options = new[] { "Option 1", "Option 2", "Option 3" }; | |
| private readonly string[] options = new[] { "Option 1", "Option 2", "Option 3" }; |
| ShowLineNumbers="true" /> | ||
|
|
||
| @code { | ||
| private string codeSnippet = @" |
There was a problem hiding this comment.
Field 'codeSnippet' can be 'readonly'.
| private string codeSnippet = @" | |
| private readonly string codeSnippet = @" |
…d KeyboardEventManager tests
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 56 out of 59 changed files in this pull request and generated 19 comments.
Files not reviewed (1)
- website/package-lock.json: Language not supported
Comments suppressed due to low confidence (1)
src/RazorConsole.Website/RazorConsoleRenderer.cs:162
- This assignment to consoleOption is useless, since its value is never read.
| @@ -0,0 +1,83 @@ | |||
| console.log('main.js loaded'); | |||
There was a problem hiding this comment.
Debug console.log statement should be removed before merging.
| console.log("Terminal host element is not available"); | ||
| return; | ||
| } | ||
|
|
||
| const term = new Terminal({ | ||
| fontFamily: "'Cascadia Code', 'Fira Code', Consolas, 'Courier New', monospace", | ||
| fontSize: 14, | ||
| lineHeight: 1.2, | ||
| cursorBlink: true, | ||
| scrollback: 1000, | ||
| cursorInactiveStyle: 'none', | ||
| theme: isDark ? TERMINAL_THEME.dark : TERMINAL_THEME.light, | ||
| allowProposedApi: true | ||
| }); | ||
|
|
||
| xtermRef.current = term; | ||
|
|
||
| const disposeSafely = () => { | ||
| if (disposed) { | ||
| return; | ||
| } | ||
| disposed = true; | ||
| term.dispose(); | ||
| xtermRef.current = null; | ||
| }; | ||
| console.log("Initializing terminal preview for", elementId); |
There was a problem hiding this comment.
Debug console.log statements should be removed before merging.
| console.Profile.Width = 80; | ||
| console.Profile.Height = 150; | ||
|
|
||
| var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150)); |
There was a problem hiding this comment.
The variable consoleOption is declared but never used. Consider removing it or using it if it was intended for something.
|
|
||
| const module = await moduleCache.get(absoluteUrl)!; | ||
|
|
||
| console.log(`Loaded .NET module from ${absoluteUrl}`); |
There was a problem hiding this comment.
Debug console.log statement should be removed before merging.
| <ItemGroup> | ||
| <!-- use .net10 ASP.NET --> | ||
| <!-- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" VersionOverride="10.0.0" /> --> | ||
| <!-- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" VersionOverride="10.0.0" PrivateAssets="all" /> --> |
There was a problem hiding this comment.
Commented-out package references should be removed if not needed, or uncommented if they are needed. Leaving commented code reduces maintainability.
| public partial class Registry | ||
| { | ||
| private static readonly Dictionary<string, IRazorConsoleRenderer> _renderers = new(); | ||
| private static readonly HashSet<string> _subscriptions = new(); |
There was a problem hiding this comment.
The _subscriptions field is declared but never used. Consider removing it if it's not needed for future functionality.
| const previewRegistry: Record<string, PreviewDefinition> = { | ||
| Align: { | ||
| tagName: 'razor-console-align', | ||
| componentType: 'RazorConsole.Website.RazorConsoleComponents.AlignDemo, RazorConsole.Website' | ||
| } | ||
| } |
There was a problem hiding this comment.
The previewRegistry only contains a single component 'Align', but the system appears designed to support multiple components based on the structure in Program.cs which has a switch statement for many components (Align, Border, Scrollable, etc.). This registry appears to be incomplete or unused. Consider removing this code if it's not needed, or properly implementing it to match the actual component registration system.
| Console.WriteLine("Program.cs loaded"); | ||
| [SupportedOSPlatform("browser")] | ||
| public partial class Registry | ||
| { | ||
| private static readonly Dictionary<string, IRazorConsoleRenderer> _renderers = new(); | ||
| private static readonly HashSet<string> _subscriptions = new(); | ||
|
|
||
| [JSExport] | ||
| [SupportedOSPlatform("browser")] | ||
| public static void RegisterComponent(string elementID) | ||
| { | ||
| Console.WriteLine(elementID); |
There was a problem hiding this comment.
Debug Console.WriteLine statements should be removed or converted to proper logging before merging.
| { | ||
| throw new InvalidOperationException("The renderer has not been initialized."); | ||
| } | ||
| var sw = new StringWriter(); |
There was a problem hiding this comment.
Disposable 'StringWriter' is created but not disposed.
…ET WASM components
| console.log(`Getting existing terminal: ${elementId}`); | ||
| const terminal = getTerminalInstance(elementId); | ||
| console.log('Terminal found:', terminal); | ||
| if (!terminal) { | ||
| throw new Error(`Terminal with id '${elementId}' has not been initialized.`); | ||
| } | ||
| return terminal; | ||
| } | ||
| export function isTerminalAvailable() { | ||
| return typeof window !== 'undefined' && typeof window.Terminal === 'function'; | ||
| } | ||
|
|
||
| export function registerTerminalInstance(elementId, terminal) { | ||
| terminalInstances.set(elementId, terminal); | ||
| } | ||
|
|
||
| export function getTerminalInstance(elementId) { | ||
| return terminalInstances.get(elementId); | ||
| } | ||
| /** | ||
| * @param {string} elementId | ||
| * @param {RazorConsoleTerminalOptions | undefined} options | ||
| * @returns {import('xterm').Terminal} | ||
| */ | ||
| export function initTerminal(elementId, options) { | ||
| const TerminalCtor = getTerminalConstructor(); | ||
| const host = ensureHostElement(elementId); | ||
| disposeTerminal(elementId); | ||
| const mergedOptions = { | ||
| ...defaultOptions, | ||
| ...options, | ||
| theme: mergeThemes(defaultOptions.theme, options?.theme) | ||
| }; | ||
|
|
||
| console.log('Merged terminal options:', mergedOptions); |
There was a problem hiding this comment.
Console.log statements should not be present in production code. Consider using a proper logging library or removing debug statements before merging.
Debug logs found at lines 38, 66, 68, and 100.
| return () => { | ||
| cancelled = true; | ||
| if (disposeTimer !== null) { | ||
| clearTimeout(disposeTimer); | ||
| } | ||
| disposeTimer = window.setTimeout(disposeSafely, 0); | ||
| }; |
There was a problem hiding this comment.
The variable disposeTimer is declared but never assigned or used except for cleanup. The setTimeout is called but the result is assigned to disposeTimer which is then immediately checked in the return cleanup. This creates a potential memory leak if the component is unmounted quickly.
Consider simplifying the disposal logic or ensuring the timer is properly tracked:
let disposeTimeoutId: ReturnType<typeof setTimeout> | null = null;
// ...
return () => {
cancelled = true;
if (disposeTimeoutId !== null) {
clearTimeout(disposeTimeoutId);
}
disposeTimeoutId = setTimeout(disposeSafely, 0);
};| const load = async (currentUrl: string): Promise<any> => { | ||
| const absoluteUrl = new URL(currentUrl, window.location.origin).toString(); | ||
|
|
||
| if (!moduleCache.has(absoluteUrl)) { | ||
| const modulePromise = (async () => { | ||
| const response = await fetch(absoluteUrl, { cache: 'no-cache' }); | ||
| if (!response.ok) { | ||
| throw new Error(`Failed to fetch ${absoluteUrl}: ${response.status} ${response.statusText}`); | ||
| } | ||
|
|
||
| const source = await response.text(); | ||
| const blobUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' })); | ||
| try { | ||
| return await import(/* @vite-ignore */ blobUrl); | ||
| } finally { | ||
| URL.revokeObjectURL(blobUrl); | ||
| } | ||
| })().catch(error => { | ||
| moduleCache.delete(absoluteUrl); | ||
| throw error; | ||
| }); | ||
|
|
||
| moduleCache.set(absoluteUrl, modulePromise); | ||
| } | ||
|
|
||
| const module = await moduleCache.get(absoluteUrl)!; | ||
|
|
||
| console.log(`Loaded .NET module from ${absoluteUrl}`); | ||
|
|
||
| const { getAssemblyExports, getConfig } = await module | ||
| .dotnet | ||
| .withDiagnosticTracing(false) | ||
| .create(); | ||
|
|
||
| const config = getConfig(); | ||
| const exports = await getAssemblyExports(config.mainAssemblyName); | ||
| return exports; | ||
| }; |
There was a problem hiding this comment.
The load function is defined but never called. This appears to be dead code that should be removed or integrated into the component logic.
| scrollback: 1000, | ||
| cols: 80, | ||
| rows: 150, | ||
| rendererType: 'dom' |
There was a problem hiding this comment.
The rendererType: 'dom' option is deprecated in xterm.js. The renderer type is automatically chosen based on browser capabilities in newer versions.
Consider removing this option to avoid potential issues with future versions of xterm.js.
| public void OnCompleted() | ||
| { | ||
| return; |
There was a problem hiding this comment.
The OnCompleted method has an empty return statement which is unnecessary for a void method. Simply remove the return statement or make the method body empty.
public void OnCompleted()
{
}| _serviceProvider = services.BuildServiceProvider(); | ||
| _consoleRenderer = _serviceProvider.GetRequiredService<ConsoleRenderer>(); | ||
| _keyboardEventManager = _serviceProvider.GetRequiredService<KeyboardEventManager>(); | ||
| var focusManager = _serviceProvider.GetRequiredService<FocusManager>(); | ||
|
|
||
| _ansiConsole = AnsiConsole.Create(new AnsiConsoleSettings | ||
| { | ||
| Ansi = AnsiSupport.Yes, | ||
| ColorSystem = ColorSystemSupport.Standard, | ||
| Out = new AnsiConsoleOutput(_sw) | ||
| }); | ||
|
|
||
| _ansiConsole.Profile.Capabilities.Unicode = true; | ||
|
|
||
| _ansiConsole.Profile.Width = 80; | ||
| _ansiConsole.Profile.Height = 150; | ||
| var snapshot = await _consoleRenderer.MountComponentAsync<TComponent>(ParameterView.Empty, default).ConfigureAwait(false); | ||
| _consoleRenderer.Subscribe(this); | ||
| _consoleRenderer.Subscribe(focusManager); | ||
|
|
||
| var initialView = ConsoleViewResult.FromSnapshot(snapshot); | ||
| var canvas = new LiveDisplayCanvas(_ansiConsole); | ||
| var consoleLiveDisplayContext = new ConsoleLiveDisplayContext(canvas, _consoleRenderer, initialView); | ||
| var focusSession = focusManager.BeginSession(consoleLiveDisplayContext, initialView, CancellationToken.None); | ||
| await focusSession.InitializationTask.ConfigureAwait(false); | ||
| canvas.Refreshed += () => | ||
| { | ||
| var output = _sw.ToString(); | ||
| SnapshotRendered?.Invoke(output); | ||
| XTermInterop.WriteToTerminal(_componentId, output); | ||
| _sw.GetStringBuilder().Clear(); | ||
| }; | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Render Razor component to ANSI string that can be sent to xterm.js. | ||
| /// </summary> | ||
| /// <returns></returns> | ||
| public async Task<string> RenderAsync() | ||
| { | ||
| await EnsureInitializedAsync().ConfigureAwait(false); | ||
|
|
||
| if (_consoleRenderer is null) | ||
| { | ||
| throw new InvalidOperationException("The renderer has not been initialized."); | ||
| } | ||
| var sw = new StringWriter(); | ||
| var console = AnsiConsole.Create(new AnsiConsoleSettings | ||
| { | ||
| Ansi = AnsiSupport.Yes, | ||
| ColorSystem = ColorSystemSupport.Standard, | ||
| Out = new AnsiConsoleOutput(sw) | ||
| }); | ||
|
|
||
| console.Profile.Width = 80; | ||
| console.Profile.Height = 150; | ||
|
|
||
| var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150)); | ||
|
|
||
| var snapshot = await _consoleRenderer.MountComponentAsync<TComponent>(ParameterView.Empty, CancellationToken.None); | ||
|
|
||
| try | ||
| { | ||
| console.Write(snapshot.Renderable!); | ||
| } | ||
| catch (Exception ex) | ||
| { | ||
| Console.WriteLine($"Error rendering component: {ex.Message} {ex.StackTrace}"); | ||
|
|
||
| throw; | ||
| } | ||
|
|
||
| return sw.ToString(); | ||
| } | ||
|
|
||
| /// <summary> | ||
| /// Processes a keyboard event from the browser. | ||
| /// </summary> | ||
| public async Task HandleKeyboardEventAsync(string xtermKey, string domKey, bool ctrlKey, bool altKey, bool shiftKey) | ||
| { | ||
| await EnsureInitializedAsync().ConfigureAwait(false); | ||
|
|
||
| if (_keyboardEventManager is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var keyInfo = ParseKeyFromBrowser(xtermKey, domKey, ctrlKey, altKey, shiftKey); | ||
| Console.WriteLine($"Parsed ConsoleKeyInfo: KeyChar='{keyInfo.KeyChar}', Key={keyInfo.Key}, Modifiers={keyInfo.Modifiers}"); |
There was a problem hiding this comment.
Console.log and Console.WriteLine statements should not be present in production code. Consider using a proper logging framework or removing debug statements.
Debug logs found at lines 105, 130, 172, 193.
| catch (Exception ex) | ||
| { | ||
| Console.WriteLine($"Error rendering component: {ex.Message} {ex.StackTrace}"); | ||
| throw; | ||
| } |
There was a problem hiding this comment.
The error handling catches all exceptions but only writes a generic message to Console.WriteLine. This makes debugging difficult and the error message is not user-friendly.
Consider:
- Using a proper logging framework instead of Console.WriteLine
- Providing more specific error messages based on the exception type
- Potentially notifying the user through the UI when rendering fails
| console.Profile.Width = 80; | ||
| console.Profile.Height = 150; | ||
|
|
||
| var consoleOption = new RenderOptions(console.Profile.Capabilities, new Size(80, 150)); |
There was a problem hiding this comment.
This assignment to consoleOption is useless, since its value is never read.
| <Markup Content="Use arrow keys to navigate and Enter to select." Foreground="Color.Green" /> | ||
| </Rows> | ||
| @code { | ||
| private string[] options = new[] { "Option 1", "Option 2", "Option 3" }; |
There was a problem hiding this comment.
Field 'options' can be 'readonly'.
| ShowLineNumbers="true" /> | ||
|
|
||
| @code { | ||
| private string codeSnippet = @" |
There was a problem hiding this comment.
Field 'codeSnippet' can be 'readonly'.
🚀 Preview DeploymentA preview build has been generated for this PR! Download the artifact: To view the preview locally:
Live Preview URL: https://f81fef77.razorconsole.pages.dev The live preview will be automatically updated when you push new |
| load(url) | ||
| .then(exports => setDotNet(exports)) | ||
| .finally(() => setLoading(false)); |
There was a problem hiding this comment.
The error handling in the load function catches the error and removes it from moduleCache, but the error is then re-thrown. However, in the useEffect, the promise rejection is not caught with .catch(), which could lead to unhandled promise rejections.
Add error handling in the useEffect:
load(url)
.then(exports => setDotNet(exports))
.catch(error => {
console.error('Failed to load .NET module:', error);
// Optionally set an error state
})
.finally(() => setLoading(false));| public void OnNext(ConsoleRenderer.RenderSnapshot value) | ||
| { | ||
| try | ||
| { | ||
| if (value.Renderable is null) | ||
| { | ||
| return; | ||
| } | ||
|
|
||
| var output = _sw.ToString(); | ||
| SnapshotRendered?.Invoke(output); | ||
| XTermInterop.WriteToTerminal(_componentId, output); | ||
| _sw.GetStringBuilder().Clear(); | ||
| } |
There was a problem hiding this comment.
In the OnNext method, when rendering the component, the output is retrieved from _sw (the instance's StringWriter), but then a new StringWriter named sw is created in the RenderAsync method but never used in OnNext. This appears to be leftover code.
Additionally, there's a logic issue: the OnNext method should be writing to the _ansiConsole which uses _sw, but it looks like the rendering setup in InitializeAsync should handle the writing. The current implementation in OnNext might not be correctly triggering the rendering pipeline.
| switch (elementID) | ||
| { | ||
| case "Align": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Align_1>(elementID); | ||
| break; | ||
| case "Border": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Border_1>(elementID); | ||
| break; | ||
| case "Scrollable": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Scrollable_1>(elementID); | ||
| break; | ||
| case "Columns": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Columns_1>(elementID); | ||
| break; | ||
| case "Rows": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Rows_1>(elementID); | ||
| break; | ||
| case "Grid": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Grid_1>(elementID); | ||
| break; | ||
| case "Padder": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Padder_1>(elementID); | ||
| break; | ||
| case "TextButton": | ||
| _renderers[elementID] = new RazorConsoleRenderer<TextButton_1>(elementID); | ||
| break; | ||
| case "TextInput": | ||
| _renderers[elementID] = new RazorConsoleRenderer<TextInput_1>(elementID); | ||
| break; | ||
| case "Select": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Select_1>(elementID); | ||
| break; | ||
| case "Markup": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Markup_1>(elementID); | ||
| break; | ||
| case "Markdown": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Markdown_1>(elementID); | ||
| break; | ||
| case "Panel": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Panel_1>(elementID); | ||
| break; | ||
| case "Figlet": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Figlet_1>(elementID); | ||
| break; | ||
| case "SyntaxHighlighter": | ||
| _renderers[elementID] = new RazorConsoleRenderer<SyntaxHighlighter_1>(elementID); | ||
| break; | ||
| case "Table": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Table_1>(elementID); | ||
| break; | ||
| case "Spinner": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Spinner_1>(elementID); | ||
| break; | ||
| case "Newline": | ||
| _renderers[elementID] = new RazorConsoleRenderer<Newline_1>(elementID); | ||
| break; | ||
| case "SpectreCanvas": | ||
| _renderers[elementID] = new RazorConsoleRenderer<SpectreCanvas_1>(elementID); | ||
| break; | ||
| case "BarChart": | ||
| _renderers[elementID] = new RazorConsoleRenderer<BarChart_1>(elementID); | ||
| break; | ||
| case "BreakdownChart": | ||
| _renderers[elementID] = new RazorConsoleRenderer<BreakdownChart_1>(elementID); | ||
| break; | ||
| } |
There was a problem hiding this comment.
Inconsistent error handling: If an unrecognized elementID is passed to RegisterComponent, the method silently does nothing. This makes debugging difficult since callers won't know why their component didn't register.
Consider logging a warning or throwing an exception:
default:
Console.WriteLine($"Warning: Unknown component type '{elementID}'");
// or throw new ArgumentException($"Unknown component type: {elementID}", nameof(elementID));
break;| <ItemGroup> | ||
| <!-- use .net10 ASP.NET --> | ||
| <!-- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" VersionOverride="10.0.0" /> --> | ||
| <!-- <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" VersionOverride="10.0.0" PrivateAssets="all" /> --> | ||
| </ItemGroup> |
There was a problem hiding this comment.
[nitpick] The commented-out package references suggest uncertainty about the required dependencies. These should either be uncommented if needed, or removed entirely to avoid confusion.
If these packages are required for .NET 10 ASP.NET WebAssembly support, they should be uncommented. If not, remove the comments and add a note in documentation about the dependency strategy.
| @@ -0,0 +1,83 @@ | |||
| console.log('main.js loaded'); | |||
There was a problem hiding this comment.
Console logging should not be left in production code. Multiple console.log statements are present throughout the codebase (e.g., lines 1, 66, 68, 100, 130). These should be removed or replaced with a proper logging mechanism that can be disabled in production.
Consider using a logging utility or removing these debug statements before merging.
|
|
||
| if (!moduleCache.has(absoluteUrl)) { | ||
| const modulePromise = (async () => { | ||
| const response = await fetch(absoluteUrl, { cache: 'no-cache' }); |
There was a problem hiding this comment.
Security concern: The { cache: 'no-cache' } option in the fetch call disables HTTP caching, but for immutable module code, caching would improve performance.
Consider using a more appropriate cache strategy:
const response = await fetch(absoluteUrl, {
cache: 'force-cache' // or 'default' for standard caching behavior
});If you need fresh content during development but want caching in production, use a build-time environment variable to control this behavior.
| useEffect(() => { | ||
| if (dotnetUrl.current !== url) { // safeguard to prevent double-loading | ||
| setLoading(true); | ||
| dotnetUrl.current = url; | ||
| load(url) | ||
| .then(exports => setDotNet(exports)) | ||
| .finally(() => setLoading(false)); | ||
| } | ||
| }, [url]); |
There was a problem hiding this comment.
The useDotNet hook contains a potential race condition. If the url prop changes rapidly or the component unmounts before the load operation completes, this could lead to setting state on an unmounted component or loading the wrong module.
Consider:
- Adding cleanup logic to cancel pending loads when
urlchanges or component unmounts - Using an abort controller or checking if the component is still mounted before calling
setDotNet - Clearing the
dotnetUrl.currenton unmount to properly reset state
| const blobUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' })); | ||
| try { | ||
| return await import(/* @vite-ignore */ blobUrl); | ||
| } finally { | ||
| URL.revokeObjectURL(blobUrl); | ||
| } |
There was a problem hiding this comment.
Memory leak: The blob URL created with URL.createObjectURL in the finally block is revoked immediately after import, but if the import fails before reaching the finally block, the blob URL will never be revoked, causing a memory leak.
Consider restructuring to ensure cleanup:
let blobUrl: string | undefined;
try {
blobUrl = URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
return await import(/* @vite-ignore */ blobUrl);
} finally {
if (blobUrl) {
URL.revokeObjectURL(blobUrl);
}
}| scrollback: 1000, | ||
| cols: 80, | ||
| rows: 150, | ||
| rendererType: 'dom' |
There was a problem hiding this comment.
The rendererType option is being set to 'dom', but this is not a valid option according to the xterm.js API. The xterm.js library uses rendererType: 'dom' | 'canvas' in older versions, but in newer versions this option has been removed in favor of automatic selection.
Since xterm 5.x is being used, this property might be ignored or cause issues. Verify if this is necessary and remove it if not supported in the current version.
| public static void RegisterComponent(string elementID) | ||
| { | ||
| Console.WriteLine(elementID); |
There was a problem hiding this comment.
Missing null check: The method doesn't validate that elementID is not null or empty before using it as a dictionary key and creating renderers. This could lead to unexpected behavior or exceptions.
Add validation:
[JSExport]
public static void RegisterComponent(string elementID)
{
if (string.IsNullOrEmpty(elementID))
{
throw new ArgumentException("Element ID cannot be null or empty", nameof(elementID));
}
Console.WriteLine(elementID);
switch (elementID)
{
// ...
}
}
#156
Recording.2025-11-23.000044.mp4