Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
11 changes: 11 additions & 0 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,17 @@
<StaticWebAssetBuildCompressAllAssets>true</StaticWebAssetBuildCompressAllAssets>
<StaticWebAssetPublishCompressAllAssets>true</StaticWebAssetPublishCompressAllAssets>
</PropertyGroup>
<!-- Register .mjs (ES module chunks emitted by Vite code-splitting) so
Microsoft.NET.Sdk.StaticWebAssets generates an endpoint for each one
in the MapStaticAssets manifest. Without this, .mjs files silently
drop out of the manifest and MapStaticAssets returns 404 for them. -->
<ItemGroup>
<StaticWebAssetContentTypeMapping
Include="text/javascript"
Cache="$(StaticWebAssetEndpointDefaultMediaCacheControlHeader)"
Pattern="*.mjs"
Priority="1" />
</ItemGroup>
<!-- .NET SDK analyzers only work on net5.0+ -->
<PropertyGroup Condition="'$(TargetFramework)' != 'netstandard2.0'">
<AnalysisLevel>latest-all</AnalysisLevel>
Expand Down
23 changes: 22 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using System.Text;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;
Expand All @@ -13,6 +14,7 @@ public sealed class HtmlFileInertiaPageRenderer : IInertiaPageRenderer
private const string PagePlaceholder = "<!--INERTIA_PAGE_DATA-->";
private const string NoncePlaceholder = "<!--CSP_NONCE-->";
private const string VersionPlaceholder = "<!--DEPLOY_VERSION-->";
private const string ModuleCssPlaceholder = "<!--MODULE_CSS_LINKS-->";

private readonly string _beforePlaceholder;
private readonly string _afterPlaceholder;
Expand All @@ -34,6 +36,16 @@ public HtmlFileInertiaPageRenderer(IWebHostEnvironment env)
StringComparison.Ordinal
);

// Inject <link> 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(
Expand Down Expand Up @@ -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("<link rel=\"stylesheet\" href=\"/")
.Append(cssPath)
.Append("?v=")
.Append(version)
.Append("\" />");
}
return sb.ToString();
}

private static string TransformForViteDev(string html)
{
var importMapStart = html.IndexOf("<script type=\"importmap\"", StringComparison.Ordinal);
Expand Down
10 changes: 0 additions & 10 deletions framework/SimpleModule.Hosting/SimpleModuleHostExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using Microsoft.AspNetCore.Diagnostics.HealthChecks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.HttpOverrides;
using Microsoft.AspNetCore.StaticFiles;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Diagnostics;
using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -204,15 +203,6 @@ public static async Task UseSimpleModuleInfrastructure(this WebApplication app)
UseStaticFileCaching(app);
app.MapStaticAssets();

// Fallback for .mjs chunks not in the MapStaticAssets manifest.
// MapStaticAssets only knows about files present at publish time;
// mismatched builds or Vite watch rebuilds can produce chunks it misses.
{
var provider = new FileExtensionContentTypeProvider();
provider.Mappings[".mjs"] = "application/javascript";
app.UseStaticFiles(new StaticFileOptions { ContentTypeProvider = provider });
}

app.UseAuthentication();
app.UseAuthorization();
app.UseSimpleModuleRateLimiting();
Expand Down
Loading
Loading