diff --git a/.dockerignore b/.dockerignore index 8a207bac..b4ccbaaf 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,6 +2,14 @@ **/bin/ **/obj/ **/node_modules/ +**/.vite/ + +# Vite output directories — regenerated from source by `npm run build` in +# Dockerfile stage 4. Excluding these prevents stale local build artifacts +# (e.g. files left over from a previous `assetFileNames` convention) from +# being baked into the image. +modules/*/src/SimpleModule.*/wwwroot/ +template/SimpleModule.Host/wwwroot/js/ # Version control .git/ diff --git a/Directory.Build.props b/Directory.Build.props index 411d56d4..48d3d6b1 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -36,6 +36,17 @@ true true + + + + latest-all diff --git a/Makefile b/Makefile index e406833e..fb3b6255 100644 --- a/Makefile +++ b/Makefile @@ -276,6 +276,14 @@ ci-full: check build-all test-all ## Full CI: lint, build (.NET + JS), all tests docker-build: ## Build the Docker image docker build -t simplemodule . +.PHONY: docker-build-nocache +docker-build-nocache: ## Build the Docker image with no layer cache (guaranteed fresh) + docker build --no-cache -t simplemodule . + +.PHONY: docker-clean-run +docker-clean-run: docker-build ## Build a fresh Docker image and start the stack + docker compose up -d --force-recreate + .PHONY: docker-up docker-up: ## Start all Docker Compose services docker compose up -d @@ -295,9 +303,18 @@ docker-ps: ## Show running Docker Compose services # ─── Clean ─────────────────────────────────────── .PHONY: clean -clean: ## Clean .NET build outputs +clean: ## Clean .NET + Vite build outputs, static asset manifests, and Vite caches dotnet clean + @echo "Removing bin/ and obj/ directories..." find . -type d \( -name bin -o -name obj \) -not -path './node_modules/*' -exec rm -rf {} + 2>/dev/null || true + @echo "Removing module wwwroot build outputs (Vite does not clean these — emptyOutDir is false)..." + find modules -type f \( -name '*.mjs' -o -name '*.mjs.map' -o -name '*.js' -o -name '*.js.map' -o -name '*.css' -o -name '*.css.map' \) -path '*/src/SimpleModule.*/wwwroot/*' -delete 2>/dev/null || true + @echo "Removing ClientApp bundle..." + rm -f $(HOST_PROJECT)/wwwroot/js/app.js $(HOST_PROJECT)/wwwroot/js/app.js.map + @echo "Removing Vite caches..." + find . -type d -name '.vite' -not -path './node_modules/*' -exec rm -rf {} + 2>/dev/null || true + rm -rf node_modules/.vite 2>/dev/null || true + @echo "Clean complete." .PHONY: clean-js clean-js: ## Remove node_modules and JS build outputs @@ -308,6 +325,10 @@ clean-js: ## Remove node_modules and JS build outputs clean-all: clean clean-js db-reset ## Full clean (.NET + JS + database) @echo "All build artifacts and database removed." +.PHONY: clean-run +clean-run: clean build-js ## Clean everything, rebuild all JS, then run the host + dotnet run --project $(HOST_PROJECT) + .PHONY: pristine pristine: clean-all setup ## Clean everything and reinstall from scratch diff --git a/framework/SimpleModule.Hosting/Inertia/HtmlFileInertiaPageRenderer.cs b/framework/SimpleModule.Hosting/Inertia/HtmlFileInertiaPageRenderer.cs index 6581efbd..0406cf2c 100644 --- a/framework/SimpleModule.Hosting/Inertia/HtmlFileInertiaPageRenderer.cs +++ b/framework/SimpleModule.Hosting/Inertia/HtmlFileInertiaPageRenderer.cs @@ -1,3 +1,4 @@ +using System.Text; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; @@ -13,6 +14,7 @@ public sealed class HtmlFileInertiaPageRenderer : IInertiaPageRenderer private const string PagePlaceholder = ""; private const string NoncePlaceholder = ""; private const string VersionPlaceholder = ""; + private const string ModuleCssPlaceholder = ""; private readonly string _beforePlaceholder; private readonly string _afterPlaceholder; @@ -34,6 +36,16 @@ public HtmlFileInertiaPageRenderer(IWebHostEnvironment env) StringComparison.Ordinal ); + // Inject tags for every module RCL that ships its own CSS file + // (e.g. _content/SimpleModule.PageBuilder/pagebuilder.css). Discovered + // once at startup by walking the WebRootFileProvider, which sees both + // the host's physical wwwroot and every RCL's static web assets. + html = html.Replace( + ModuleCssPlaceholder, + BuildModuleCssLinks(env, InertiaMiddleware.Version), + StringComparison.Ordinal + ); + var idx = html.IndexOf(PagePlaceholder, StringComparison.Ordinal); if (idx < 0) throw new InvalidOperationException( @@ -81,6 +93,41 @@ public Task RenderPageAsync(HttpContext httpContext, string pageJson) ); } + private static string BuildModuleCssLinks(IWebHostEnvironment env, string version) + { + var contents = env.WebRootFileProvider.GetDirectoryContents("_content"); + if (!contents.Exists) + return string.Empty; + + var sb = new StringBuilder(); + foreach (var entry in contents) + { + if ( + !entry.IsDirectory + || !entry.Name.StartsWith("SimpleModule.", StringComparison.Ordinal) + ) + continue; + + // Module Vite builds emit CSS as {assembly}.lowercase().css by convention + // (see define-module-config.ts assetFileNames). The URL must match the + // on-disk filename, so ToLowerInvariant is the correct behavior here, not + // the security-focused ToUpperInvariant that CA1308 suggests. +#pragma warning disable CA1308 + var cssFileName = entry.Name.ToLowerInvariant() + ".css"; +#pragma warning restore CA1308 + var cssPath = $"_content/{entry.Name}/{cssFileName}"; + if (!env.WebRootFileProvider.GetFileInfo(cssPath).Exists) + continue; + + sb.Append(""); + } + return sb.ToString(); + } + private static string TransformForViteDev(string html) { var importMapStart = html.IndexOf("