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("Items"); + moduleCsproj.Should().NotContain("SimpleModule.Items"); + + var contractsCsproj = File.ReadAllText( + Path.Combine( + rootDir, + "src", + "modules", + "Items", + "src", + "Items.Contracts", + "Items.Contracts.csproj" + ) + ); + contractsCsproj.Should().Contain("Items.Contracts"); + } + + [Fact] + public void Scaffold_EventRecordDerivesFromDomainEvent() + { + // Issue #218: IEvent requires EventId/OccurredAt, so generated event records must derive + // from DomainEvent (which supplies them) rather than implementing IEvent directly. + var (_, rootDir) = ScaffoldStandalone(); + + var eventFile = File.ReadAllText( + Path.Combine( + rootDir, + "src", + "modules", + "Items", + "src", + "Items.Contracts", + "Events", + "ItemCreatedEvent.cs" + ) + ); + eventFile.Should().Contain(": DomainEvent"); + eventFile.Should().NotContain(": IEvent"); + } + + [Fact] + public void RootPackageJson_PinsNpmDependenciesToNpmVersion_NotFrameworkVersion() + { + // Issue #219: the @simplemodule/* npm packages are not always published in lockstep with + // NuGet, so the scaffold must pin them to the resolved npm version independently. + var templates = new Templates.ProjectTemplates( + solution: null, + frameworkVersion: "0.0.40", + npmVersion: "0.0.36" + ); + + var packageJson = templates.RootPackageJson("TestApp", frameworkPackagesPath: null); + + packageJson.Should().Contain("\"@simplemodule/client\": \"^0.0.36\""); + packageJson.Should().Contain("\"@simplemodule/ui\": \"^0.0.36\""); + packageJson.Should().Contain("\"@simplemodule/theme-default\": \"^0.0.36\""); + packageJson.Should().NotContain("0.0.40"); + } }