From 3453d995bdd80299ffba38dab1fb35ff2f25d3c5 Mon Sep 17 00:00:00 2001 From: yasmoradi Date: Wed, 5 Aug 2026 10:50:16 +0200 Subject: [PATCH 1/3] improve bit Boilerplate tooling (#12820) --- .devcontainer/devcontainer.json | 1 - .github/workflows/bit.full.ci.yml | 6 +- .../.devcontainer/devcontainer.json | 3 +- .../.docs/19- Project Miscellaneous Files.md | 2 - .../Bit.Boilerplate/.docs/20- .NET Aspire.md | 2 +- .../Boilerplate/Bit.Boilerplate/.gitignore | 2 +- .../Boilerplate/Bit.Boilerplate/.mcp.json | 53 ++-- .../Bit.Boilerplate/.vscode/extensions.json | 3 +- .../Bit.Boilerplate/.vscode/mcp.json | 21 +- .../Bit.Boilerplate/.vscode/settings.json | 3 +- .../Bit.Boilerplate/.vscode/tasks.json | 37 +-- .../Boilerplate/Bit.Boilerplate/AGENTS.md | 15 +- .../Bit.Boilerplate/aspire.config.json | 12 + .../Common/Acknowledgements.razor.cs | 1 - .../Boilerplate.Client.Web.csproj | 14 +- .../Bit.Boilerplate/src/Directory.Build.props | 8 +- .../Services/AppCertificateService.cs | 4 +- .../.aspire/settings.json | 7 - .../Boilerplate.Server.AppHost/Program.cs | 5 +- .../Properties/launchSettings.json | 4 +- .../Boilerplate.Server.Web.csproj | 6 +- .../Services/FileWatcherService.cs | 245 ++++++++++++++++++ .../Services/ScssCompilerService.cs | 102 -------- .../Program.Middlewares.cs | 2 +- .../Server/Boilerplate.Server.Web/Program.cs | 2 +- .../Properties/launchSettings.json | 14 +- .../Templates03GettingStartedPage.razor | 31 ++- .../Templates03GettingStartedPage.razor.cs | 3 - .../Templates05CreateProjectPage.razor.cs | 2 +- .../Templates/Templates06RunProjectPage.razor | 5 +- 30 files changed, 371 insertions(+), 244 deletions(-) create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/aspire.config.json delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.AppHost/.aspire/settings.json create mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/FileWatcherService.cs delete mode 100644 src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/ScssCompilerService.cs diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index b9c0341227..ecf931489b 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -26,7 +26,6 @@ "vscode": { "extensions": [ "GitHub.copilot", - "glenn2223.live-sass", "GitHub.copilot-chat", "ms-dotnettools.csharp", "ms-dotnettools.csdevkit", diff --git a/.github/workflows/bit.full.ci.yml b/.github/workflows/bit.full.ci.yml index aef44e5665..63e3f5a871 100644 --- a/.github/workflows/bit.full.ci.yml +++ b/.github/workflows/bit.full.ci.yml @@ -322,9 +322,9 @@ jobs: - name: Build sample configuration 1 run: | - dotnet new bit-bp --name TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --module Admin --offlineDb --appInsights --sentry --signalR --notification --cloudflare --ads --aspire --redis --multitenant --brouter - dotnet build TestProject/TestProject.sln -p:InvariantGlobalization=false -p:Environment=Staging -p:EnforceCodeStyleInBuild=true - rm -r "TestProject" + dotnet new bit-bp --name MyCompany.TestProject --database SqlServer --filesStorage AzureBlobStorage --api Integrated --captcha reCaptcha --pipeline Azure --module Admin --offlineDb --appInsights --sentry --signalR --notification --cloudflare --ads --aspire --redis --multitenant --brouter + dotnet build MyCompany.TestProject/MyCompany.TestProject.sln -p:InvariantGlobalization=false -p:Environment=Staging -p:EnforceCodeStyleInBuild=true + rm -r "MyCompany.TestProject" - name: Build sample configuration 2 run: | diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.devcontainer/devcontainer.json b/src/Templates/Boilerplate/Bit.Boilerplate/.devcontainer/devcontainer.json index 7b5515f814..0969055176 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.devcontainer/devcontainer.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.devcontainer/devcontainer.json @@ -16,7 +16,6 @@ "vscode": { "extensions": [ "GitHub.copilot", - "glenn2223.live-sass", "GitHub.copilot-chat", "ms-dotnettools.csharp", "ms-dotnettools.csdevkit", @@ -50,4 +49,4 @@ "remoteEnv": { "ASPNETCORE_ENVIRONMENT": "Development" } -} \ No newline at end of file +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/19- Project Miscellaneous Files.md b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/19- Project Miscellaneous Files.md index 1d8abffb36..ffa07a8058 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/19- Project Miscellaneous Files.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/19- Project Miscellaneous Files.md @@ -472,7 +472,6 @@ You can add additional MCP servers to extend Copilot's capabilities further. For ```json { - "liveSassCompile.settings.watchOnLaunch": true, "dotnet.defaultSolution": "Boilerplate.Web.slnf", "dotnet.unitTests.runSettingsPath": "src/Tests/.runsettings", "chat.tools.autoApprove": true, @@ -505,7 +504,6 @@ You can add additional MCP servers to extend Copilot's capabilities further. For { "recommendations": [ "GitHub.copilot", - "glenn2223.live-sass", "GitHub.copilot-chat", "ms-dotnettools.csharp", "ms-dotnettools.csdevkit", diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/20- .NET Aspire.md b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/20- .NET Aspire.md index 1f421dcbe1..a2f003582c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.docs/20- .NET Aspire.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.docs/20- .NET Aspire.md @@ -256,7 +256,7 @@ if (builder.ExecutionContext.IsRunMode) ### Trade-offs -✅ Starting the project with `F5` / `dotnet run` becomes noticeably faster +✅ Starting the project with `F5` / `aspire start` becomes noticeably faster ✅ The automated tests run considerably faster, as they no longer wait for the containers to boot up and become healthy ⚠️ Those containers keep running (and consuming memory) while you're not debugging — stop them from **Docker Desktop** (or with `docker stop`) whenever you need those resources back, the app host will start them again on the next run diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.gitignore b/src/Templates/Boilerplate/Bit.Boilerplate/.gitignore index de01a09fc0..f31f2b22f1 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.gitignore +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.gitignore @@ -231,4 +231,4 @@ custom.aprof /src/Client/Boilerplate.Client.Maui/Platforms/Android/google-services.json .env -.playwright-mcp +.playwright-mcp/ diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.mcp.json b/src/Templates/Boilerplate/Bit.Boilerplate/.mcp.json index 549882d096..d3ce3c80da 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.mcp.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.mcp.json @@ -1,21 +1,38 @@ { - "mcpServers": { - "MicrosoftLearn": { - "type": "http", - "url": "https://learn.microsoft.com/api/mcp" - }, - "DeepWiki": { - "type": "http", - "url": "https://mcp.deepwiki.com/mcp" - }, - "bitBlazorUI": { - "type": "http", - "url": "https://blazorui.bitplatform.dev/mcp" - }, - "playwright": { - "type": "stdio", - "command": "npx", - "args": ["-y", "@playwright/mcp@latest"] + "mcpServers": { + "MicrosoftLearn": { + "type": "http", + "url": "https://learn.microsoft.com/api/mcp" + }, + "DeepWiki": { + "type": "http", + "url": "https://mcp.deepwiki.com/mcp" + }, + "bitBlazorUI": { + "type": "http", + "url": "https://blazorui.bitplatform.dev/mcp" + }, + //#if (aspire == true) + "aspire": { + "type": "stdio", + "command": "aspire", + "args": [ + "agent", + "mcp" + ] + }, + //#endif + "playwright": { + "type": "stdio", + "command": "npx", + "args": [ + "-y", + "@playwright/mcp@latest", + "--output-dir", + ".playwright-mcp", + "--output-max-size", + "52428800" + ] + } } - } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/extensions.json b/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/extensions.json index 0640b9cf4f..00bd707f77 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/extensions.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/extensions.json @@ -1,7 +1,6 @@ { "recommendations": [ "GitHub.copilot", - "glenn2223.live-sass", "GitHub.copilot-chat", "ms-dotnettools.csharp", "ms-dotnettools.csdevkit", @@ -12,4 +11,4 @@ "ms-dotnettools.blazorwasm-companion", "ms-dotnettools.vscode-dotnet-runtime" ] -} \ No newline at end of file +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/mcp.json b/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/mcp.json index 9056bf25b2..c41962bb41 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/mcp.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/mcp.json @@ -12,10 +12,27 @@ "type": "http", "url": "https://blazorui.bitplatform.dev/mcp" }, + //#if (aspire == true) + "aspire": { + "type": "stdio", + "command": "aspire", + "args": [ + "agent", + "mcp" + ] + }, + //#endif "playwright": { "type": "stdio", "command": "npx", - "args": ["-y", "@playwright/mcp@latest"] + "args": [ + "-y", + "@playwright/mcp@latest", + "--output-dir", + "${workspaceFolder}/.playwright-mcp", + "--output-max-size", + "52428800" + ] } } -} \ No newline at end of file +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/settings.json b/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/settings.json index a0d9f10d1b..7a9022241a 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/settings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/settings.json @@ -1,7 +1,6 @@ { "files.encoding": "utf8", "files.autoGuessEncoding": false, - "liveSassCompile.settings.watchOnLaunch": true, "dotnet.defaultSolution": "Boilerplate.Web.slnf", "dotnet.unitTests.runSettingsPath": "src/Tests/.runsettings", "chat.tools.autoApprove": true, @@ -17,7 +16,7 @@ "*.json": "$(capture).*.json, $(capture).*.json", "AGENTS.md": "CLAUDE.md, GEMINI.md, COPILOT.md" }, - "workbench.editorAssociations": { + "workbench.editorAssociations": { "*.md": "vscode.markdown.preview.editor" } } diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/tasks.json b/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/tasks.json index be62282468..b5896694be 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/tasks.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.vscode/tasks.json @@ -4,43 +4,10 @@ { "label": "before-build", "type": "shell", - "command": "dotnet build src/Client/Boilerplate.Client.Core/Boilerplate.Client.Core.csproj -t:BeforeBuildTasks", + "command": "dotnet build Boilerplate.slnx -t:BeforeBuildTasks", "runOptions": { "runOn": "folderOpen" } - }, - { - "label": "build", - "type": "shell", - "command": "dotnet build src/Server/Boilerplate.Server.Web/Boilerplate.Server.Web.csproj" - }, - { - "label": "generate-resx-files", - "type": "shell", - "command": "dotnet build src/Shared/Boilerplate.Shared.csproj -t:PrepareResources" - }, - { - "label": "run", - "type": "shell", - "command": "dotnet", - "args": [ - "run" - ], - "options": { - "cwd": "${workspaceFolder}/src/Server/Boilerplate.Server.Web" - }, - "isBackground": true - }, - { - "label": "run-tests", - "type": "shell", - "command": "dotnet", - "args": [ - "test" - ], - "options": { - "cwd": "${workspaceFolder}/src/Tests" - } } ] -} \ No newline at end of file +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/AGENTS.md b/src/Templates/Boilerplate/Bit.Boilerplate/AGENTS.md index 8a15c5abef..109fdeae7c 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/AGENTS.md +++ b/src/Templates/Boilerplate/Bit.Boilerplate/AGENTS.md @@ -68,15 +68,15 @@ Before implementing any changes, you **MUST** complete the following: ## 4. Critical Command Reference -- **Build the project**: Run `dotnet build` in Boilerplate.Server.AppHost project root directory. -- **Run the project**: Run `dotnet watch` in Boilerplate.Server.AppHost project root directory. If needed, you may use the Playwright MCP tools to interact with the running UI to validate things (navigate, click, fill forms, take screenshots), and use `browser_evaluate` to run in-page JavaScript to accelerate the process (e.g. quickly locating elements, extracting data, or asserting state). +- **Build the project**: Run `dotnet build` in src/Server/Boilerplate.Server.AppHost project directory. +- **Run the project**: Run `aspire start`. If needed, you may use the Playwright MCP tools to interact with the `serverweb` resource running by aspire to validate things (navigate, click, fill forms, take screenshots), and use `browser_evaluate` to run in-page JavaScript to accelerate the process (e.g. quickly locating elements, extracting data, or asserting state). -- **Build the project**: Run `dotnet build` in Boilerplate.Server.Web project root directory. -- **Run the project**: Run `dotnet watch` in Boilerplate.Server.Web project root directory. If needed, you may use the Playwright MCP tools to interact with the running UI to validate things (navigate, click, fill forms, take screenshots), and use `browser_evaluate` to run in-page JavaScript to accelerate the process (e.g. quickly locating elements, extracting data, or asserting state). +- **Build the project**: Run `dotnet build` in src/Server/Boilerplate.Server.Web project directory. +- **Run the project**: Run `dotnet watch` in src/Server/Boilerplate.Server.Web project directory. If needed, you may use the Playwright MCP tools to interact with the running UI to validate things (navigate, click, fill forms, take screenshots), and use `browser_evaluate` to run in-page JavaScript to accelerate the process (e.g. quickly locating elements, extracting data, or asserting state). -- **Run tests**: Run `dotnet test` in Boilerplate.Tests project root directory. -- **Add new migrations**: Run `dotnet ef migrations add --output-dir Infrastructure/Data/Migrations --verbose` in Boilerplate.Server.Api project root directory. -- **Generate Resx C# code**: Run `dotnet build -t:PrepareResources` in Boilerplate.Shared project root directory. +- **Run tests**: Run `dotnet test` in src/Tests/Boilerplate.Tests project directory. +- **Add new migrations**: Run `dotnet ef migrations add --output-dir Infrastructure/Data/Migrations --verbose` in src/Server/Boilerplate.Server.Api project directory. +- **Generate Resx C# code**: Run `dotnet build -t:PrepareResources` in src/Shared/Boilerplate.Shared project directory. ## 5. Coding Conventions & Best Practices @@ -93,6 +93,7 @@ Before implementing any changes, you **MUST** complete the following: - **Style Bit.BlazorUI Components Correctly**: Use the `::deep` selector in your `.scss` files to style `Bit.BlazorUI` components. - **Use Theme Colors in C# and Razor**: In C# and Razor files, you **MUST** use `BitColor` enum and `BitCss` class to apply theme colors instead of hardcoded colors. Use `BitColor` for component parameters (e.g., `BitColor.Primary`, `BitColor.TertiaryBackground`). Use `BitCss.Class` for CSS classes (e.g., `@BitCss.Class.Color.Background.Primary`, `@BitCss.Class.Color.Foreground.Secondary`). Use `BitCss.Var` for inline styles with CSS variables (e.g., `border-color:var(@BitCss.Var.Color.Border.Primary)`). This ensures automatic dark/light mode support. - **Use Theme Colors in SCSS**: In SCSS files, you **MUST** use SCSS variables from `_bit-css-variables.scss` instead of hardcoded colors. Import the file and use variables like `$bit-color-primary`, `$bit-color-foreground-primary`, `$bit-color-background-secondary`, etc. These map to CSS custom properties that automatically adapt to dark/light modes. Available variable categories include: primary, secondary, tertiary, info, success, warning, severe-warning, error, foreground, background, border, and neutral colors. +- **Localize User-Visible Text with String Literals**: When you add or change ANY user-visible text, you **MUST NOT** add, rename, or edit keys in `.resx` files - `.resx` changes are not supported by hot reload (`dotnet watch` / `aspire start`) and force a full restart. Instead, use the `IStringLocalizer` string indexer with the literal English text, e.g. `Localizer["Welcome back {0}!", user.GetDisplayName()]` or `@Localizer["Product saved successfully."]`. `Localizer[nameof(AppStrings.X)]` stays as-is for existing keys whose text you are not changing; do not create a duplicate literal for them. Only when the user **explicitly** asks to apply translations, move all such literals into `resx` files, and switch the call sites back to `Localizer[nameof(AppStrings.X)]`. - **Use Enhanced Lifecycle Methods**: In components inheriting from `AppComponentBase` or pages inheriting from `AppPageBase`, you **MUST** use `OnInitAsync`, `OnParamsSetAsync`, and `OnAfterFirstRenderAsync`. - **WrapHandled**: Use `WrapHandled` for event handlers in razor files to prevent unhandled exceptions. Example 1: `OnClick="WrapHandled(MyMethod)"` instead of `OnClick="MyMethod"`. diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/aspire.config.json b/src/Templates/Boilerplate/Bit.Boilerplate/aspire.config.json new file mode 100644 index 0000000000..ad5a1a6d05 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/aspire.config.json @@ -0,0 +1,12 @@ +{ + "appHost": { + "path": "src/Server/Boilerplate.Server.AppHost/Boilerplate.Server.AppHost.csproj" + }, + "features": { + "deployCommandEnabled": true, + "execCommandEnabled": true, + "defaultWatchEnabled": true, + "aspireSkillsRemoteFetchEnabled": true, + "terminalCommandsEnabled": true + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Common/Acknowledgements.razor.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Common/Acknowledgements.razor.cs index 6d46e20a26..93ad17249e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Common/Acknowledgements.razor.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Core/Components/Common/Acknowledgements.razor.cs @@ -142,7 +142,6 @@ private record Dependency(string Name, string? Url, string? Repo, string License new("Container Tools (Docker)", "https://marketplace.visualstudio.com/items?itemName=ms-azuretools.vscode-docker", "https://github.com/microsoft/vscode-docker", "MIT"), new("Dev Containers", "https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers", null, "Proprietary"), new("Blazor WASM Companion", "https://marketplace.visualstudio.com/items?itemName=ms-dotnettools.blazorwasm-companion", "https://github.com/dotnet/razor", "MIT"), - new("Live Sass Compiler", "https://marketplace.visualstudio.com/items?itemName=glenn2223.live-sass", "https://github.com/glenn2223/vscode-live-sass-compiler", "MIT"), new("ResX Manager", "https://marketplace.visualstudio.com/items?itemName=TomEnglert.ResXManager", "https://github.com/dotnet/ResXResourceManager", "MIT"), new("VS \"ASP.NET & web development\" workload", "https://visualstudio.microsoft.com/vs/features/web", null, "Proprietary"), new("VS \".NET MAUI development\" workload", "https://dotnet.microsoft.com/apps/maui", null, "Proprietary"), diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Boilerplate.Client.Web.csproj b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Boilerplate.Client.Web.csproj index 53cb7d46b2..4b94bd18b9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Boilerplate.Client.Web.csproj +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Client/Boilerplate.Client.Web/Boilerplate.Client.Web.csproj @@ -18,10 +18,6 @@ $(ResolveStaticWebAssetsInputsDependsOn) - false - false - false - $(Environment) false @@ -75,19 +71,19 @@ Blazor WebAssembly diagnostics https://learn.microsoft.com/en-us/aspnet/core/blazor/performance/webassembly-event-pipe-diagnostics Put the followings inside Boilerplate.Client.Web/wwwroot/Boilerplate.Client.Web.lib.module.js - export function afterWebAssemblyStarted(options, extensions) { + export function afterWebAssemblyStarted(options, extensions) { globalThis.getDotnetRuntime(0).collectCpuSamples({durationSeconds: 10}); // Or un-comment the following line to collect metrics instead of CPU samples - // globalThis.getDotnetRuntime(0).collectMetrics({durationSeconds: 10}); + // globalThis.getDotnetRuntime(0).collectMetrics({durationSeconds: 10}); } - + Or run this command in the browser console to collect GC dump: globalThis.getDotnetRuntime(0).collectGcDump(); Convert nettrace file to json using dotnet-trace convert your-file-name.nettrace -f Chromium --> - @@ -79,7 +83,7 @@ - - - - - - + diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/FileWatcherService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/FileWatcherService.cs new file mode 100644 index 0000000000..871066cac6 --- /dev/null +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/FileWatcherService.cs @@ -0,0 +1,245 @@ +using System.Text; + +namespace Boilerplate.Server.Web.Infrastructure.Services; + +/// +/// Watches the source files of every project in the solution during development and runs the MSBuild targets +/// that own those files (SCSS to CSS, TypeScript to JavaScript, resx to C#, ...) as soon as they change. +/// +public sealed class FileWatcherService : IDisposable +{ + /// + /// The file patterns to watch and the MSBuild targets to run for each of them. + /// A project is watched for a pattern only when its csproj defines (or depends on) the corresponding target. + /// + private static readonly Dictionary targetsByFilePattern = new() + { + { "*.scss", ["BuildCssFiles"] }, // Checkout + /// The targets in 's declaration order, which is the order they're run in. + /// + private static readonly string[] orderedTargets = [.. targetsByFilePattern.Values.SelectMany(targets => targets).Distinct(StringComparer.Ordinal)]; + + /// + /// Directories that hold no hand written source file, but do hold plenty of files matching the patterns above. + /// + private static readonly string[] ignoredDirectories = ["node_modules", "bin", "obj", ".git", ".vs"]; + + /// + /// Editors save a single file with several write operations, so the events are coalesced before the targets are run. + /// + private static readonly TimeSpan debounceDelay = TimeSpan.FromMilliseconds(250); + + internal static void Start(WebApplication app) + { + if (app.Environment.IsDevelopment() is false) + return; + + var logger = app.Services.GetRequiredService>(); + + // Environment.CurrentDirectory is the Server.Web project's directory during development, so ../../ is the solution's src directory. + var srcDirectory = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "../../")); + + if (Directory.Exists(srcDirectory) is false) + { + logger.LogWarning("{Directory} not found, no source file is watched", srcDirectory); + return; + } + + foreach (var projectPath in Directory.EnumerateFiles(srcDirectory, "*.csproj", SearchOption.AllDirectories)) + { + if (IsIgnored(srcDirectory, projectPath)) + continue; + + var projectContent = File.ReadAllText(projectPath); + + var watchList = targetsByFilePattern + .Select(item => (Pattern: item.Key, Targets: item.Value.Where(target => projectContent.Contains(target, StringComparison.Ordinal)).ToArray())) + .Where(item => item.Targets.Length > 0) + .ToArray(); + + if (watchList.Length is 0) + continue; + + var watcherService = new FileWatcherService(logger, projectPath); + app.Lifetime.ApplicationStopping.Register(watcherService.Dispose); + + foreach (var (pattern, targets) in watchList) + { + watcherService.Watch(pattern, targets); + } + + logger.LogInformation("Watching {Patterns} of {Project}", string.Join(", ", watchList.Select(item => item.Pattern)), projectPath); + } + } + + private readonly ILogger logger; + private readonly string projectPath; + private readonly string projectDirectory; + private readonly List watchers = []; + private readonly HashSet pendingTargets = new(StringComparer.Ordinal); + private readonly SemaphoreSlim runLock = new(1, 1); + private readonly Lock pendingTargetsLock = new(); + private readonly Timer debounceTimer; + private bool disposed; + + private FileWatcherService(ILogger logger, string projectPath) + { + this.logger = logger; + this.projectPath = projectPath; + projectDirectory = Path.GetDirectoryName(projectPath)!; + debounceTimer = new Timer(_ => _ = RunPendingTargetsAsync(), null, Timeout.InfiniteTimeSpan, Timeout.InfiniteTimeSpan); + } + + private void Watch(string pattern, string[] targets) + { + var watcher = new FileSystemWatcher(projectDirectory, pattern) + { + IncludeSubdirectories = true, + InternalBufferSize = 64 * 1024, + NotifyFilter = NotifyFilters.LastWrite | NotifyFilters.FileName | NotifyFilters.Size + }; + + watcher.Changed += (_, e) => OnFileChanged(e.FullPath, targets); + watcher.Created += (_, e) => OnFileChanged(e.FullPath, targets); + watcher.Deleted += (_, e) => OnFileChanged(e.FullPath, targets); + watcher.Renamed += (_, e) => OnFileChanged(e.FullPath, targets); + watcher.Error += (_, e) => logger.LogError(e.GetException(), "Watching {Pattern} of {Project} failed", pattern, projectPath); + + watcher.EnableRaisingEvents = true; + + watchers.Add(watcher); + } + + private void OnFileChanged(string filePath, string[] targets) + { + if (IsIgnored(projectDirectory, filePath)) + return; + + lock (pendingTargetsLock) + { + if (disposed) + return; + + foreach (var target in targets) + { + pendingTargets.Add(target); + } + + debounceTimer.Change(debounceDelay, Timeout.InfiniteTimeSpan); + } + } + + private async Task RunPendingTargetsAsync() + { + string[] targets; + + lock (pendingTargetsLock) + { + if (pendingTargets.Count is 0) + return; + + targets = [.. orderedTargets.Where(pendingTargets.Contains)]; + pendingTargets.Clear(); + } + + // Files changing while a previous run is still in progress must not run two MSBuild processes against the same project. + await runLock.WaitAsync(); + + try + { + foreach (var target in targets) + { + await RunTargetAsync(target); + } + } + finally + { + runLock.Release(); + } + } + + private async Task RunTargetAsync(string target) + { + try + { + using var process = new Process + { + StartInfo = new ProcessStartInfo + { + WorkingDirectory = projectDirectory, + FileName = "dotnet", + RedirectStandardOutput = true, + RedirectStandardError = true, + CreateNoWindow = true, + UseShellExecute = false + } + }; + process.StartInfo.ArgumentList.Add("build"); + process.StartInfo.ArgumentList.Add(projectPath); + process.StartInfo.ArgumentList.Add($"-t:{target}"); + process.StartInfo.ArgumentList.Add("--no-restore"); + process.StartInfo.ArgumentList.Add("--no-dependencies"); + + StringBuilder outputResult = new(); + StringBuilder errorResult = new(); + + process.OutputDataReceived += (_, e) => { if (string.IsNullOrEmpty(e.Data) is false) outputResult.AppendLine(e.Data); }; + process.ErrorDataReceived += (_, e) => { if (string.IsNullOrEmpty(e.Data) is false) errorResult.AppendLine(e.Data); }; + + if (process.Start() is false) + { + logger.LogError("Failed to run the {Target} target of {Project}", target, projectPath); + return; + } + + process.BeginOutputReadLine(); + process.BeginErrorReadLine(); + await process.WaitForExitAsync(); + + if (process.ExitCode is 0) + { + logger.LogInformation("Ran the {Target} target of {Project}:\n{Output}", target, projectPath, outputResult.ToString()); + } + else + { + logger.LogError("The {Target} target of {Project} failed with exit code {ExitCode}:\n{Output}{Errors}", target, projectPath, process.ExitCode, outputResult.ToString(), errorResult.ToString()); + } + } + catch (Exception ex) + { + logger.LogError(ex, "Failed to run the {Target} target of {Project}", target, projectPath); + } + } + + private static bool IsIgnored(string rootDirectory, string filePath) + { + return Path.GetRelativePath(rootDirectory, filePath) + .Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar) + .Any(segment => ignoredDirectories.Contains(segment, StringComparer.OrdinalIgnoreCase)); + } + + public void Dispose() + { + lock (pendingTargetsLock) + { + if (disposed) + return; + + disposed = true; + } + + debounceTimer.Dispose(); + + foreach (var watcher in watchers) + { + watcher.Dispose(); + } + + watchers.Clear(); + } +} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/ScssCompilerService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/ScssCompilerService.cs deleted file mode 100644 index a5be4a5404..0000000000 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/ScssCompilerService.cs +++ /dev/null @@ -1,102 +0,0 @@ -using Meziantou.Framework.Win32; - -namespace Boilerplate.Server.Web.Infrastructure.Services; - -/// -/// This compiles SCSS files into CSS in real-time during development, no extensions required. -/// -public class ScssCompilerService -{ - internal static async Task WatchScssFiles(WebApplication app) - { - if (app.Environment.IsDevelopment() is false) - return; - - if (Environment.GetEnvironmentVariable("IN_APP_SCSS_COMPILER_ENABLED") is not "true") - return; // Check out Visual Studio's launchSettings.json - - if (OperatingSystem.IsWindows() is false) - return; - - var clientCorePath = Path.Combine(Environment.CurrentDirectory, "../../Client/Boilerplate.Client.Core"); - - var logger = app.Services.GetRequiredService>(); - - var toolPath = Path.Combine(clientCorePath, "node_modules/.bin/sass.cmd"); - - if (File.Exists(toolPath) is false) - { - logger.LogWarning("{SassTool} not found", toolPath); - return; - } - - // The ScssCompilerService operates from the Client.Core directory. - // Using .:., it locates all .scss files in the `Styles` and `Components` folders of Client.Core (see --load-path), - // compiles them to CSS, and saves them alongside the .scss files. - // It also compiles app.scss to app.css and places it in the wwwroot/styles folder. - // For Client.Web, Client.Maui, or Client.Windows, if present, the process is similar, - // but uses ../../Client/Boilerplate.Client.Web:../../Client/Boilerplate.Client.Web instead of .:., - // as the working directory remains Client.Core. - - var sassPathsToWatch = new List - { - "Components:Components", "Styles/app.scss:wwwroot/styles/app.css" - }; - - if (Path.Exists(Path.Combine(Environment.CurrentDirectory, "../../Client/Boilerplate.Client.Maui/Components"))) - sassPathsToWatch.Add("../../Client/Boilerplate.Client.Maui/Components:../../Client/Boilerplate.Client.Maui/Components"); - - if (Path.Exists(Path.Combine(Environment.CurrentDirectory, "../../Client/Boilerplate.Client.Windows/Components"))) - sassPathsToWatch.Add("../../Client/Boilerplate.Client.Windows/Components:../../Client/Boilerplate.Client.Windows/Components"); - - if (Path.Exists(Path.Combine(Environment.CurrentDirectory, "../../Client/Boilerplate.Client.Web/Components"))) - sassPathsToWatch.Add("../../Client/Boilerplate.Client.Web/Components:../../Client/Boilerplate.Client.Web/Components"); - - var command = $"{string.Join(" ", sassPathsToWatch)} --style compressed --silence-deprecation=import --update --watch --color"; - - // Create a job object to ensure the child process terminates with the parent - using var job = new JobObject(); - job.SetLimits(new JobObjectLimits - { - Flags = JobObjectLimitFlags.KillOnJobClose // Terminate process when job is closed - }); - - using var watchScssFilesProcess = new Process - { - StartInfo = new ProcessStartInfo - { - WorkingDirectory = clientCorePath, - FileName = toolPath, - Arguments = command, - RedirectStandardOutput = true, - RedirectStandardError = true, - CreateNoWindow = true - } - }; - - watchScssFilesProcess.OutputDataReceived += (_, e) => { if (string.IsNullOrEmpty(e.Data) is false) logger.LogInformation(e.Data); }; - watchScssFilesProcess.ErrorDataReceived += (_, e) => { if (string.IsNullOrEmpty(e.Data) is false) logger.LogError(e.Data); }; - - logger.LogInformation("Running {toolPath} for {ProjectDirectory}", toolPath, clientCorePath); - if (watchScssFilesProcess.Start() is false) - { - logger.LogError("Failed to start {toolPath} for {ProjectDirectory}", toolPath, clientCorePath); - return; - } - - // Assign the process to the job - try - { - job.AssignProcess(watchScssFilesProcess); - } - catch (Exception ex) - { - logger.LogError(ex, "Failed to assign process to job for {toolPath}", toolPath); - } - - watchScssFilesProcess.BeginOutputReadLine(); - watchScssFilesProcess.BeginErrorReadLine(); - - await app.WaitForShutdownAsync(); - } -} diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs index 0b92de0560..55e7e63c0e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.Middlewares.cs @@ -63,7 +63,7 @@ public void ConfigureMiddlewares() { context.Response.GetTypedHeaders().CacheControl = new() { - NoCache = true + NoStore = true }; } else diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs index 84c9b1152b..5eaf8d9a8e 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs @@ -46,7 +46,7 @@ public static async Task Main(string[] args) //-:cnd:noEmit #if Development - _ = ScssCompilerService.WatchScssFiles(app); + FileWatcherService.Start(app); #endif //+:cnd:noEmit diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Properties/launchSettings.json b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Properties/launchSettings.json index 865bce6b62..b1cda5fec0 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Properties/launchSettings.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Properties/launchSettings.json @@ -4,8 +4,7 @@ "commandName": "Project", "launchBrowser": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "IN_APP_SCSS_COMPILER_ENABLED": "true" + "ASPNETCORE_ENVIRONMENT": "Development" }, "dotnetRunMessages": true, "applicationUrl": "http://*:5030" @@ -27,8 +26,7 @@ "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development", - "WebAppRender:BlazorMode": "BlazorWebAssembly", - "IN_APP_SCSS_COMPILER_ENABLED": "true" + "WebAppRender:BlazorMode": "BlazorWebAssembly" }, "dotnetRunMessages": true, "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", @@ -61,16 +59,14 @@ "commandName": "IISExpress", "launchBrowser": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "IN_APP_SCSS_COMPILER_ENABLED": "true" + "ASPNETCORE_ENVIRONMENT": "Development" } }, "IIS": { "commandName": "IIS", "launchBrowser": true, "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development", - "IN_APP_SCSS_COMPILER_ENABLED": "true" + "ASPNETCORE_ENVIRONMENT": "Development" } } }, @@ -85,4 +81,4 @@ } }, "$schema": "http://json.schemastore.org/launchsettings.json" -} \ No newline at end of file +} diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor index e5ebe60fc4..25352df3eb 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor @@ -224,25 +224,24 @@ rm $HOME/dotnet.tar.gz } } - VS Code -
- -
+ @if (showCrossPlatform) + { + VS Code +
+ +
+ } - \ No newline at end of file + diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs index cfe3fe9e97..9f4ee80f83 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates03GettingStartedPage.razor.cs @@ -95,9 +95,6 @@ public partial class Templates03GettingStartedPage (text: @"echo 'Install the GitHub Copilot extension for Visual Studio Code https://marketplace.visualstudio.com/items?itemName=GitHub.copilot';", command: "code --install-extension GitHub.copilot;"), - (text: @"echo 'Install the Live Sass Compiler extension for Visual Studio Code https://marketplace.visualstudio.com/items?itemName=glenn2223.live-sass';", - command: "code --install-extension glenn2223.live-sass;"), - (text: @"echo 'Install the GitHub Copilot Chat extension for Visual Studio Code https://marketplace.visualstudio.com/items?itemName=GitHub.copilot-chat';", command: "code --install-extension GitHub.copilot-chat;"), diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs index fbb8cfca27..84008f0cc5 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs @@ -17,7 +17,7 @@ public partial class Templates05CreateProjectPage private Parameter redis = new() { Value = false, Default = false }; private Parameter aspire = new() { Value = true, Default = true }; private Parameter multiTenant = new() { Value = true, Default = true }; - private Parameter brouter = new() { Value = true, Default = true }; + private Parameter brouter = new() { Value = true, Default = false }; private Parameter captcha = new() { diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates06RunProjectPage.razor b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates06RunProjectPage.razor index 57a33cca2d..c44558c1e2 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates06RunProjectPage.razor +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates06RunProjectPage.razor @@ -47,8 +47,7 @@ The project is configured to use Blazor Server as the default mode to enhance the development experience, enabling features such as Visual Studio .NET Hot Reload and dotnet watch. To utilize the Hot Reload feature, - ensure that you have the latest versions of Visual Studio, the .NET SDK, - and Live Sass Compiler for VS Code installed. + ensure that you have the latest versions of Visual Studio and the .NET SDK. This setup allows for automatic application of changes made to .razor, .scss, or .cs files without the need to rebuild the project. If any changes to these files do not take effect without a rebuild, please feel free to create an issue here. @@ -215,4 +214,4 @@

- \ No newline at end of file + From fb4f1e105dfca4a3e14d86eb2a79867bd715eaac Mon Sep 17 00:00:00 2001 From: yasmoradi Date: Wed, 5 Aug 2026 10:59:23 +0200 Subject: [PATCH 2/3] fix --- .../Boilerplate/Bit.Boilerplate/.template.config/template.json | 2 +- .../Pages/Templates/Templates05CreateProjectPage.razor.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json index 5067559692..4b0875a7ee 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json +++ b/src/Templates/Boilerplate/Bit.Boilerplate/.template.config/template.json @@ -245,7 +245,7 @@ "displayName": "Use bit Brouter?", "type": "parameter", "datatype": "bool", - "defaultValue": "true", + "defaultValue": "false", "description": "Replaces the built-in Blazor Router with bit Brouter, bringing advanced routing features - nested routes, route guards, data loaders, keep-alive pages and view transitions - inspired by the routers of Vue.js, React and Angular." }, "nameToLower": { diff --git a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs index 84008f0cc5..77a87d7a46 100644 --- a/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs +++ b/src/Websites/Platform/src/Bit.Websites.Platform.Client/Pages/Templates/Templates05CreateProjectPage.razor.cs @@ -17,7 +17,7 @@ public partial class Templates05CreateProjectPage private Parameter redis = new() { Value = false, Default = false }; private Parameter aspire = new() { Value = true, Default = true }; private Parameter multiTenant = new() { Value = true, Default = true }; - private Parameter brouter = new() { Value = true, Default = false }; + private Parameter brouter = new() { Value = false, Default = false }; private Parameter captcha = new() { From 01bef80005e97d5ef0ea94f272a89f0a309bb500 Mon Sep 17 00:00:00 2001 From: yasmoradi Date: Wed, 5 Aug 2026 11:31:47 +0200 Subject: [PATCH 3/3] fix --- .../Infrastructure/Services/FileWatcherService.cs | 4 ++-- .../src/Server/Boilerplate.Server.Web/Program.cs | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/FileWatcherService.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/FileWatcherService.cs index 871066cac6..6ed30be5e9 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/FileWatcherService.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Infrastructure/Services/FileWatcherService.cs @@ -34,7 +34,7 @@ public sealed class FileWatcherService : IDisposable /// private static readonly TimeSpan debounceDelay = TimeSpan.FromMilliseconds(250); - internal static void Start(WebApplication app) + internal static async Task StartAsync(WebApplication app) { if (app.Environment.IsDevelopment() is false) return; @@ -55,7 +55,7 @@ internal static void Start(WebApplication app) if (IsIgnored(srcDirectory, projectPath)) continue; - var projectContent = File.ReadAllText(projectPath); + var projectContent = await File.ReadAllTextAsync(projectPath); var watchList = targetsByFilePattern .Select(item => (Pattern: item.Key, Targets: item.Value.Where(target => projectContent.Contains(target, StringComparison.Ordinal)).ToArray())) diff --git a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs index 5eaf8d9a8e..3b02afcfa6 100644 --- a/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs +++ b/src/Templates/Boilerplate/Bit.Boilerplate/src/Server/Boilerplate.Server.Web/Program.cs @@ -46,7 +46,7 @@ public static async Task Main(string[] args) //-:cnd:noEmit #if Development - FileWatcherService.Start(app); + await FileWatcherService.StartAsync(app); #endif //+:cnd:noEmit