diff --git a/Directory.Build.props b/Directory.Build.props
index 48d3d6b1..6a74e506 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -47,6 +47,16 @@
Pattern="*.mjs"
Priority="1" />
+
+
+ $(RepoRoot)packages/SimpleModule.Theme.Default/theme.css
+ $(RepoRoot)modules
+ $(RepoRoot)packages/SimpleModule.Client/src/routes.ts
+
latest-all
diff --git a/cli/SimpleModule.Cli/Commands/New/NewProjectCommand.cs b/cli/SimpleModule.Cli/Commands/New/NewProjectCommand.cs
index 82e81da9..9fd7ed70 100644
--- a/cli/SimpleModule.Cli/Commands/New/NewProjectCommand.cs
+++ b/cli/SimpleModule.Cli/Commands/New/NewProjectCommand.cs
@@ -32,6 +32,11 @@ public override int Execute(CommandContext context, NewProjectSettings settings)
solution
);
+ // The @simplemodule/* npm packages are not always published in lockstep with NuGet,
+ // so pin them to the latest published npm version rather than the framework version
+ // (otherwise `npm install` fails with notarget — see issue #219).
+ var npmVersion = NpmVersionResolver.ResolveVersion(frameworkVersion);
+
if (settings.DryRun)
{
var ops = PlanFiles(projectName, rootDir);
@@ -47,7 +52,7 @@ public override int Execute(CommandContext context, NewProjectSettings settings)
ctx =>
{
ctx.Status("Scaffolding project files...");
- ScaffoldProject(projectName, rootDir, solution, frameworkVersion);
+ ScaffoldProject(projectName, rootDir, solution, frameworkVersion, npmVersion);
}
);
@@ -70,10 +75,15 @@ public static void ScaffoldProject(
string projectName,
string rootDir,
SolutionContext? solution,
- string frameworkVersion
+ string frameworkVersion,
+ string? npmVersion = null
)
{
- var projectTemplates = new ProjectTemplates(solution, frameworkVersion);
+ var projectTemplates = new ProjectTemplates(
+ solution,
+ frameworkVersion,
+ npmVersion ?? frameworkVersion
+ );
var moduleTemplates = new ModuleTemplates(solution);
const string moduleName = "Items";
@@ -155,6 +165,10 @@ string frameworkVersion
Path.Combine(hostDir, "wwwroot", "index.html"),
HostTemplates.IndexHtml()
);
+ File.WriteAllText(
+ Path.Combine(hostDir, "wwwroot", "favicon.svg"),
+ HostTemplates.FaviconSvg()
+ );
File.WriteAllText(Path.Combine(hostDir, "ClientApp", "app.tsx"), HostTemplates.AppTsx());
File.WriteAllText(
Path.Combine(hostDir, "ClientApp", "vite.config.ts"),
@@ -309,6 +323,7 @@ string rootDir
Plan(Path.Combine(hostDir, $"{projectName}.Host.csproj"));
Plan(Path.Combine(hostDir, "Program.cs"));
Plan(Path.Combine(hostDir, "wwwroot", "index.html"));
+ Plan(Path.Combine(hostDir, "wwwroot", "favicon.svg"));
Plan(Path.Combine(hostDir, "ClientApp", "app.tsx"));
Plan(Path.Combine(hostDir, "ClientApp", "vite.config.ts"));
Plan(Path.Combine(hostDir, "ClientApp", "vite.dev.config.ts"));
diff --git a/cli/SimpleModule.Cli/Infrastructure/NpmVersionResolver.cs b/cli/SimpleModule.Cli/Infrastructure/NpmVersionResolver.cs
new file mode 100644
index 00000000..d16e0776
--- /dev/null
+++ b/cli/SimpleModule.Cli/Infrastructure/NpmVersionResolver.cs
@@ -0,0 +1,66 @@
+using System.Text.Json;
+
+namespace SimpleModule.Cli.Infrastructure;
+
+///
+/// Resolves the latest published version of the @simplemodule/* npm packages.
+/// The npm packages are not always published in lockstep with the NuGet framework version, so a
+/// scaffold must not assume the framework (NuGet) version exists on npm — otherwise
+/// npm install fails with notarget (see issue #219).
+///
+public static class NpmVersionResolver
+{
+ private static readonly Uri NpmRegistryUri = new(
+ "https://registry.npmjs.org/@simplemodule/client"
+ );
+
+ // Shared HttpClient avoids socket exhaustion from repeated instantiation.
+ private static readonly HttpClient SharedHttpClient = new()
+ {
+ Timeout = TimeSpan.FromSeconds(5),
+ };
+
+ ///
+ /// Resolves the npm version to pin the @simplemodule/* dependencies to.
+ /// Priority: npm registry dist-tags.latest > .
+ ///
+ ///
+ /// Version to use when the registry can't be reached (typically the framework version).
+ ///
+ public static string ResolveVersion(string fallbackVersion)
+ {
+ return QueryNpmLatestVersion() ?? fallbackVersion;
+ }
+
+ private static string? QueryNpmLatestVersion()
+ {
+ try
+ {
+ // Intentional sync-over-async: CLI runs single-threaded, no deadlock risk.
+ var json = SharedHttpClient.GetStringAsync(NpmRegistryUri).GetAwaiter().GetResult();
+ using var doc = JsonDocument.Parse(json);
+
+ if (
+ doc.RootElement.TryGetProperty("dist-tags", out var distTags)
+ && distTags.TryGetProperty("latest", out var latest)
+ )
+ {
+ return latest.GetString();
+ }
+ }
+ catch (HttpRequestException)
+ {
+ // Network errors — fall through
+ }
+ catch (TaskCanceledException)
+ {
+ // Timeout — fall through
+ }
+ catch (JsonException)
+ {
+ // Parse errors — fall through
+ }
+
+ return null;
+ }
+}
diff --git a/cli/SimpleModule.Cli/SimpleModule.Cli.csproj b/cli/SimpleModule.Cli/SimpleModule.Cli.csproj
index e718efb8..20592d38 100644
--- a/cli/SimpleModule.Cli/SimpleModule.Cli.csproj
+++ b/cli/SimpleModule.Cli/SimpleModule.Cli.csproj
@@ -34,6 +34,10 @@
Include="..\..\template\SimpleModule.Host\wwwroot\index.html"
LogicalName="Templates.Host.wwwroot.index.html"
/>
+
+ /// Copy favicon.svg as-is. The host's index.html links /favicon.svg and
+ /// Program.cs maps /favicon.ico to it, so the scaffold must ship the file.
+ ///
+ public static string FaviconSvg()
+ {
+ return EmbeddedResourceReader.ReadTemplate("Templates.Host.wwwroot.favicon.svg");
+ }
+
///
/// Copy app.tsx as-is.
///
diff --git a/cli/SimpleModule.Cli/Templates/ModuleTemplates.cs b/cli/SimpleModule.Cli/Templates/ModuleTemplates.cs
index ea4ae33e..89d80e35 100644
--- a/cli/SimpleModule.Cli/Templates/ModuleTemplates.cs
+++ b/cli/SimpleModule.Cli/Templates/ModuleTemplates.cs
@@ -41,7 +41,8 @@ public string ContractsCsproj(string moduleName)
ReplaceFrameworkProjectRefs(
TemplateExtractor.TransformCsproj(refPath, _refModule!, moduleName)
),
- $"SimpleModule.{moduleName}.Contracts"
+ $"SimpleModule.{moduleName}.Contracts",
+ $"{moduleName}.Contracts"
);
}
@@ -60,7 +61,8 @@ public string ModuleCsproj(string moduleName)
ReplaceFrameworkProjectRefs(
TemplateExtractor.TransformCsproj(refPath, _refModule!, moduleName, stripPatterns)
),
- $"SimpleModule.{moduleName}"
+ $"SimpleModule.{moduleName}",
+ moduleName
);
}
@@ -803,12 +805,25 @@ private static string ReplaceFrameworkProjectRefs(string csprojContent)
///
/// Ensures the csproj content has explicit RootNamespace and AssemblyName set to the
- /// expected SimpleModule.<Module> convention. This guards against the analyzer's SM0052
- /// (assembly naming) and SM0053 (matching contracts assembly) checks when the csproj
- /// filename and namespace prefix don't naturally agree.
+ /// expected convention. This guards against the analyzer's SM0052 (assembly naming) and
+ /// SM0053 (matching contracts assembly) checks when the csproj filename and namespace
+ /// prefix don't naturally agree.
///
- private static string EnsureAssemblyName(string csprojContent, string expectedName)
+ /// The RootNamespace to insert when missing.
+ ///
+ /// The AssemblyName to insert when missing. Defaults to .
+ /// The module (RCL) project passes the bare module name here so its served static-web-asset
+ /// base path (/_content/{AssemblyName}/) matches the Vite pages bundle name, which is
+ /// derived from the project directory basename.
+ ///
+ private static string EnsureAssemblyName(
+ string csprojContent,
+ string rootNamespace,
+ string? assemblyName = null
+ )
{
+ assemblyName ??= rootNamespace;
+
var hasAssembly = csprojContent.Contains("", StringComparison.Ordinal);
var hasRoot = csprojContent.Contains("", StringComparison.Ordinal);
@@ -833,12 +848,12 @@ private static string EnsureAssemblyName(string csprojContent, string expectedNa
if (!hasRoot)
{
- toInsert.Add($"{indent}{expectedName}");
+ toInsert.Add($"{indent}{rootNamespace}");
}
if (!hasAssembly)
{
- toInsert.Add($"{indent}{expectedName}");
+ toInsert.Add($"{indent}{assemblyName}");
}
lines.InsertRange(insertAt, toInsert);
@@ -967,7 +982,11 @@ private static string FallbackContractsCsproj(string moduleName) =>
net10.0
Library
SimpleModule.{moduleName}.Contracts
- SimpleModule.{moduleName}.Contracts
+
+ {moduleName}.Contracts
@@ -982,7 +1001,10 @@ private static string FallbackModuleCsproj(string moduleName) =>
net10.0
Library
SimpleModule.{moduleName}
- SimpleModule.{moduleName}
+
+ {moduleName}
@@ -1052,7 +1074,7 @@ private static string FallbackEventClass(string moduleName, string singularName)
namespace SimpleModule.{{moduleName}}.Contracts.Events;
- public sealed record {{singularName}}CreatedEvent(int {{singularName}}Id) : IEvent;
+ public sealed record {{singularName}}CreatedEvent(int {{singularName}}Id) : DomainEvent;
""";
private static string FallbackModuleClass(string moduleName, string singularName) =>
diff --git a/cli/SimpleModule.Cli/Templates/ProjectTemplates.cs b/cli/SimpleModule.Cli/Templates/ProjectTemplates.cs
index 21ad6f48..ae98a4fc 100644
--- a/cli/SimpleModule.Cli/Templates/ProjectTemplates.cs
+++ b/cli/SimpleModule.Cli/Templates/ProjectTemplates.cs
@@ -9,11 +9,21 @@ public sealed class ProjectTemplates
private const string BaseProjectName = "SimpleModule";
private readonly SolutionContext? _solution;
private readonly string _frameworkVersion;
+ private readonly string _npmVersion;
public ProjectTemplates(SolutionContext? solution, string frameworkVersion)
+ : this(solution, frameworkVersion, frameworkVersion) { }
+
+ ///
+ /// Version to pin the @simplemodule/* npm dependencies to. The npm packages are not
+ /// always published in lockstep with NuGet, so this may differ from
+ /// (see issue #219).
+ ///
+ public ProjectTemplates(SolutionContext? solution, string frameworkVersion, string npmVersion)
{
_solution = solution;
_frameworkVersion = frameworkVersion;
+ _npmVersion = npmVersion;
}
public string Slnx(string projectName)
@@ -233,7 +243,7 @@ public string GlobalJson()
public string RootPackageJson(string projectName, string? frameworkPackagesPath)
{
- return GenerateRootPackageJson(projectName, frameworkPackagesPath, _frameworkVersion);
+ return GenerateRootPackageJson(projectName, frameworkPackagesPath, _npmVersion);
}
public string BiomeJson()
@@ -689,7 +699,7 @@ private static string FallbackTestsSharedCsproj(string projectName) =>
private static string GenerateRootPackageJson(
string projectName,
string? frameworkPackagesPath,
- string frameworkVersion
+ string npmVersion
)
{
// npm 'name' field must be lowercase; workspace glob uses actual project casing.
@@ -708,9 +718,9 @@ string frameworkVersion
}
else
{
- clientDep = $"\"^{frameworkVersion}\"";
- uiDep = $"\"^{frameworkVersion}\"";
- themeDep = $"\"^{frameworkVersion}\"";
+ clientDep = $"\"^{npmVersion}\"";
+ uiDep = $"\"^{npmVersion}\"";
+ themeDep = $"\"^{npmVersion}\"";
}
return $$"""
diff --git a/framework/SimpleModule.Hosting/build/SimpleModule.Hosting.targets b/framework/SimpleModule.Hosting/build/SimpleModule.Hosting.targets
index 8e0f25bc..7949125a 100644
--- a/framework/SimpleModule.Hosting/build/SimpleModule.Hosting.targets
+++ b/framework/SimpleModule.Hosting/build/SimpleModule.Hosting.targets
@@ -1,4 +1,29 @@
+
+
+ $([MSBuild]::EnsureTrailingSlash($([MSBuild]::GetDirectoryNameOfFileAbove('$(MSBuildProjectDirectory)', 'package.json'))))
+
+
+
+ $(RepoRoot)node_modules/@simplemodule/theme-default/theme.css
+ $(RepoRoot)src/modules
+ $(MSBuildProjectDirectory)/ClientApp/routes.ts
+
-
+
@@ -95,7 +120,7 @@
Outputs="$(ExtractDtoStamp)"
>
@@ -109,7 +134,7 @@
Outputs="$(ExtractRoutesStamp)"
>
diff --git a/tasks/todo.md b/tasks/todo.md
index 6e2b86d2..825bcb10 100644
--- a/tasks/todo.md
+++ b/tasks/todo.md
@@ -1,114 +1,21 @@
-# Task: Bring source files under 300 lines
-
-Goal: every `.cs`, `.ts`, `.tsx` file in the repo under 300 lines.
-
-## Scope rules
-- **Skip** auto-generated EF Core migrations (`**/Migrations/*.cs`, `**/Migrations/*.Designer.cs`) — regenerated by `dotnet ef`, hand-edits get erased.
-- **Skip** CLI template files (`cli/SimpleModule.Cli/Templates/*.cs`) — mostly verbatim-string literals.
-- **Skip** auto-generated `packages/SimpleModule.Client/src/routes.ts` — regenerated from endpoint constants.
-- Verification: `dotnet build` (0W/0E), `npm run typecheck`, `npm run lint`.
-
-## Refactor strategies
-- **React pages** → extract sub-components into sibling `Pages/components/` folder; hooks into `.ts` files.
-- **Services (C#)** → existing `partial class` pattern; split by responsibility (e.g. `Service.Export.cs`, `Service.Logging.cs`).
-- **Non-partial C# classes** → add `partial` and split helpers/logging into sibling files.
-- **UI library components** → split multi-purpose components into focused files (`chart.tsx` → `chart-tooltip.tsx` + `chart-legend.tsx` + `chart-context.ts`).
-
-## Done (22 files, all verified)
-
-### React pages (13)
-- [x] modules/Dashboard/.../Pages/Home.tsx (601 → 214) — extracted UserInfoPanel, TokenTester, ApiTester
-- [x] modules/AuditLogs/.../Pages/Dashboard.tsx (589 → 249) — extracted KpiCard, DonutCard, HBarCard, filters, inline charts
-- [x] modules/Admin/.../Pages/Admin/UsersEdit.tsx (560 → 219) — split 4 tabs into separate components
-- [x] modules/Map/.../Pages/Browse.tsx (553 → 294) — extracted LayersPanel, BasemapsPanel, top/bottom controls, useViewportInsets hook
-- [x] modules/AuditLogs/.../Pages/Browse.tsx (484 → 187) — extracted filters, results table, pagination
-- [x] modules/AuditLogs/.../Pages/Detail.tsx (445 → 79) — extracted overview/http/domain/changes cards, correlated table, shared helpers
-- [x] modules/RateLimiting/.../Pages/Admin.tsx (404 → 112) — extracted create dialog, rules table, active policies table, badges
-- [x] modules/Email/.../Pages/History.tsx (353 → 202) — extracted filters + pagination
-- [x] modules/Map/.../Pages/Layers.tsx (342 → 202) — extracted AddLayerSourceDialog, AddBasemapDialog
-- [x] modules/OpenIddict/.../Pages/ClientsEdit.tsx (324 → 238) — extracted UriList, permission constants
-- [x] modules/Marketplace/.../Pages/Detail.tsx (316 → 255) — extracted CopyButton, CommandBlock, InfoRow, icons
-- [x] modules/Settings/.../Pages/MenuManager.tsx (306 → 266) — extracted tree helpers to menu-helpers.ts
-- [x] modules/PageBuilder/.../Pages/Editor.tsx (306 → 201) — extracted HeaderActions component
-
-### Services / framework (C#) (7)
-- [x] framework/SimpleModule.DevTools/ViteDevWatchService.cs (497 → 257) — split Logging, Helpers, Process partials
-- [x] modules/PageBuilder/.../PageBuilderService.cs (395 → 297) — split Logging, Templates partials
-- [x] modules/Map/.../MapService.cs (383 → 279) — split Logging, Basemaps partials
-- [x] modules/AuditLogs/.../AuditLogService.cs (382 → 174) — split Dashboard, Export partials
-- [x] modules/FeatureFlags/.../FeatureFlagService.cs (361 → 210) — split Resolution partial
-- [x] modules/Email/.../EmailService.cs (360 → 219) — split Templates partial
-- [x] modules/Datasets/.../DatasetsContractsService.cs (337 → 245) — split Features partial
-
-### Misc (C# + UI library) (5)
-- [x] framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs (438 → 298) — split Helpers partial
-- [x] cli/SimpleModule.Cli/Infrastructure/PortChecker.cs (319 → 127) — split Finders partial
-- [x] modules/Marketplace/.../NuGetMarketplaceService.cs (316 → 248) — extracted NuGetDtos.cs
-- [x] packages/SimpleModule.UI/components/chart.tsx (386 → 119) — split chart-context.ts, chart-tooltip.tsx, chart-legend.tsx
-- [x] packages/SimpleModule.UI/components/layouts/public-layout.tsx (364 → 83) — split public-layout-nav.tsx, public-layout-mobile.tsx
-
-## Remaining (32 files) — not done; organized for future batches
-
-### Generator internals (3 files) — needs careful refactoring
-- [ ] framework/SimpleModule.Generator/Discovery/SymbolDiscovery.cs (2068)
-- [ ] framework/SimpleModule.Generator/Emitters/DiagnosticEmitter.cs (1294)
-- [ ] framework/SimpleModule.Generator/Discovery/DiscoveryData.cs (640)
-
-**Why hold off:** Core of compile-time module discovery. Splitting these risks source-generator correctness. Recommend writing tests around current behavior first, then refactoring by concern (per-diagnostic emitters, per-symbol-type discovery).
-
-### CLI commands (2 files) — dense, doable
-- [ ] cli/SimpleModule.Cli/Commands/Dev/DevCommand.cs (777)
-- [ ] cli/SimpleModule.Cli/Commands/New/NewProjectCommand.cs (754)
-
-**Approach:** `DevCommand` orchestrates multi-process dev workflows (dotnet + npm watches) — read carefully before splitting. `NewProjectCommand` can split project scaffolding steps into separate methods in a partial.
-
-### Tests (~20 files) — mechanical but high-churn
-- [ ] tests/SimpleModule.Generator.Tests/DiagnosticTests.cs (1361) — split per-diagnostic (SM0001–SM0054) into separate test classes
-- [ ] tests/SimpleModule.Generator.Tests/HostDbContextGenerationTests.cs (1108)
-- [ ] tests/SimpleModule.DevTools.Tests/ViteDevWatchServiceTests.cs (529)
-- [ ] modules/Email/tests/SimpleModule.Email.Tests/Unit/EmailServiceTests.cs (515)
-- [ ] tests/SimpleModule.Database.Tests/EntityInterceptorTests.cs (462)
-- [ ] modules/Chat/tests/SimpleModule.Chat.Tests/Integration/ChatStreamingEndpointTests.cs (459)
-- [ ] tests/SimpleModule.Generator.Tests/ModuleExtensionsGenerationTests.cs (447)
-- [ ] tests/SimpleModule.Generator.Tests/TopologicalSortTests.cs (428)
-- [ ] modules/PageBuilder/tests/SimpleModule.PageBuilder.Tests/PageEndpointTests.cs (412)
-- [ ] tests/SimpleModule.Core.Tests/Events/EventBusPartialFailureTests.cs (401)
-- [ ] tests/SimpleModule.Generator.Tests/DtoJsonResolverGenerationTests.cs (392)
-- [ ] tests/SimpleModule.LoadTests/Infrastructure/LoadTestWebApplicationFactory.cs (385)
-- [ ] modules/FileStorage/tests/SimpleModule.FileStorage.Tests/FileStorageServiceTests.cs (381)
-- [ ] modules/Chat/tests/SimpleModule.Chat.Tests/Unit/ChatServiceTests.cs (374)
-- [ ] tests/SimpleModule.Generator.Tests/ViewDiscoveryTests.cs (365)
-- [ ] modules/BackgroundJobs/tests/.../ProgressFlushServiceLifecycleTests.cs (359)
-- [ ] tests/SimpleModule.Generator.Tests/TypeScriptDefinitionsEmitterTests.cs (356)
-- [ ] tests/SimpleModule.Cli.Tests/NewProjectScaffoldTests.cs (351)
-- [ ] tests/SimpleModule.Generator.Tests/ContractAutoDiscoveryTests.cs (322)
-- [ ] tests/SimpleModule.Tests.Shared/Fixtures/SimpleModuleWebApplicationFactory.cs (321)
-- [ ] modules/PageBuilder/tests/SimpleModule.PageBuilder.Tests/PageBuilderServiceTests.cs (321)
-- [ ] modules/Chat/tests/SimpleModule.Chat.Tests/Integration/ChatEndpointTests.cs (317)
-- [ ] tests/SimpleModule.Generator.Tests/EndpointExtensionsEmitterTests.cs (316)
-
-**Question for the user:** Do we want tests split at all? Large test classes are sometimes acceptable. If yes, split by feature area (one-class-per-endpoint or one-class-per-feature).
-
-### E2E / k6 specs (3 files)
-- [ ] tests/e2e/tests/flows/pagebuilder-crud.spec.ts (414)
-- [ ] tests/e2e/tests/flows/menu-manager-crud.spec.ts (368)
-- [ ] tests/k6/scenarios/hotspots.ts (365)
-
-**Note:** Playwright groups tests per `.spec.ts` file; splitting changes report structure. k6 `hotspots.ts` has its own scenario shape.
-
-## Lessons captured
-- Adding `using` directives is required when splitting partials that reference types the original file imported. The three initial service splits had missing usings (`Microsoft.EntityFrameworkCore`, `SimpleModule.Core`, `SimpleModule.Core.FeatureFlags`) — caught by `dotnet build` on second run.
-- `packages/SimpleModule.Client/src/routes.ts` is auto-regenerated when the build runs and sometimes reformats line wraps. Revert it before committing to keep the diff clean.
-- Enforcing under-300 going forward: consider adding a `wc -l` check to CI that blocks PRs introducing new files over 300 lines (exempting migrations, CLI templates, and auto-gen paths).
-
-## CI check candidate
-```bash
-# fails if any tracked .cs/.ts/.tsx is > 300 lines
-find . -type f \( -name "*.cs" -o -name "*.ts" -o -name "*.tsx" \) \
- ! -path "*/node_modules/*" ! -path "*/bin/*" ! -path "*/obj/*" \
- ! -path "*/dist/*" ! -path "*/wwwroot/*" ! -path "*/.git/*" \
- ! -path "*/Migrations/*" ! -path "*/cli/SimpleModule.Cli/Templates/*" \
- ! -path "*/packages/SimpleModule.Client/src/routes.ts" \
- -exec wc -l {} + | awk '$1 > 300 && $2 != "total" { count++; print }
- END { if (count > 0) exit 1 }'
-```
+# CLI Bug Fixes (GitHub issues)
+
+Branch: claude/cli-bug-fixes-2OKyG
+
+- [x] #218 Generated event records implement `IEvent` → now derive from `DomainEvent` (FallbackEventClass)
+- [x] #225 `sm new project` references `/favicon.svg` but ships none → embed + write `wwwroot/favicon.svg`
+- [x] #219 Scaffold pinned `@simplemodule/*` npm deps to framework (NuGet) version → new `NpmVersionResolver` resolves the latest *published* npm version; threaded through `ProjectTemplates`/`ScaffoldProject`
+- [x] #228 Module `AssemblyName` = `SimpleModule.X` but dir basename = `X` → bundle 404. Set module AssemblyName to bare `X` AND contracts to `X.Contracts` (required so the generator's `module + ".Contracts"` pairing still discovers contract impls — otherwise SM0025)
+- [x] #221 `SimpleModule.Hosting.targets` undefined `$(RepoRoot)` + monorepo paths → added RepoRoot fallback (nearest package.json) + overridable `SimpleModuleThemeCss`/`SimpleModuleModulesDir`/`SimpleModuleRoutesOutput` props with consumer defaults; monorepo overrides them in `Directory.Build.props` (scoped via `packages/` check)
+
+## Verification
+- [x] dotnet build CLI — succeeds
+- [x] dotnet test CLI tests — 136/136 pass (incl. scaffold `dotnet build` against published NuGet, now green)
+- [x] dotnet build template host — succeeds; integrated Vite + Tailwind build runs (validates #221 targets)
+
+## Review
+- #228 root cause was subtler than the issue described: bare module AssemblyName alone breaks
+ the generator's module↔contracts pairing (ContractFinder uses `moduleAssembly.Name + ".Contracts"`).
+ Both assembly names must go bare together; namespaces stay `SimpleModule.X(.Contracts)`.
+- #219 falls back to the framework version when the npm registry is unreachable (offline),
+ preserving deterministic test behavior (ScaffoldProject's npmVersion defaults to frameworkVersion).
diff --git a/tests/SimpleModule.Cli.Tests/NewProjectScaffoldTests.StructureTests.cs b/tests/SimpleModule.Cli.Tests/NewProjectScaffoldTests.StructureTests.cs
index 054a8728..2de433a8 100644
--- a/tests/SimpleModule.Cli.Tests/NewProjectScaffoldTests.StructureTests.cs
+++ b/tests/SimpleModule.Cli.Tests/NewProjectScaffoldTests.StructureTests.cs
@@ -99,4 +99,87 @@ public void Scaffold_WithSolution_UsesLocalPackages()
var content = File.ReadAllText(Path.Combine(rootDir, "package.json"));
content.Should().Contain("file:");
}
+
+ [Fact]
+ public void Scaffold_ShipsFaviconSvg()
+ {
+ // Issue #225: index.html links /favicon.svg and Program.cs maps /favicon.ico to it,
+ // so the scaffold must ship the file (otherwise both 404 on every page).
+ var (projectName, rootDir) = ScaffoldStandalone();
+
+ var favicon = Path.Combine(rootDir, "src", $"{projectName}.Host", "wwwroot", "favicon.svg");
+ File.Exists(favicon).Should().BeTrue();
+ File.ReadAllText(favicon).Should().Contain("