From 5a867faee63dfee6938c214668c8f8e6a6683507 Mon Sep 17 00:00:00 2001 From: Koji Wakayama Date: Tue, 11 Aug 2026 10:21:14 +0200 Subject: [PATCH] fix(css): report a missing CSS optimizer once, and say how to add one `acquireCSSGenerationSession` warned on every acquisition. `regenerateCSSByHash` acquires a session per cold-cache request, so a hosted project emitted this line once per render -- the single most frequent entry in its logs, at warn level. The message also named a registration hook the docs never mention, leaving a developer with nothing to act on. Report it once while no engine is registered, re-arming when one is observed so an engine that disappears later is still reported, and name the package that registers one, matching how `resolve()` reports a missing extension. --- .../css-provider-session.test.ts | 39 +++++++++++++++++++ src/html/styles-builder/tailwind-compiler.ts | 20 +++++++++- 2 files changed, 57 insertions(+), 2 deletions(-) diff --git a/src/html/styles-builder/css-provider-session.test.ts b/src/html/styles-builder/css-provider-session.test.ts index 66979dae02..95bf131258 100644 --- a/src/html/styles-builder/css-provider-session.test.ts +++ b/src/html/styles-builder/css-provider-session.test.ts @@ -6,6 +6,11 @@ import { type CSSProcessor, CSSProcessorName, } from "#veryfront/extensions/css/index.ts"; +import { + __resetLogRecordEmitterForTests, + __subscribeLogRecordEmitter, + type LogEntry, +} from "#veryfront/utils/logger/index.ts"; import { assertEquals } from "#veryfront/testing/assert.ts"; import { afterEach, beforeEach, describe, it } from "#veryfront/testing/bdd.ts"; import { @@ -203,6 +208,40 @@ describe("styles-builder CSS provider sessions", () => { assertEquals(generated.css, "missing-optimizer|sheet|alpha"); }); + it("reports a missing optimizer once, with the package that provides one", () => { + // `regenerateCSSByHash` acquires a session per request, so warning on every + // acquisition made this line the most frequent entry in a hosted project's + // logs -- once per render, at warn level, naming a registration hook with no + // documented way to act on it. State the package that registers an engine, + // and say it once while the engine stays absent. + installProcessor(createProcessor("warn-rearm")); + register(CSSOptimizationEngineName, createOptimizer("warn-rearm")); + // Observing an engine re-arms the warning, so this test does not depend on + // whether an earlier test in this file already reported the absence. + acquireCSSGenerationSession(true); + resetContracts(); + installProcessor(createProcessor("warn-once")); + + const records: LogEntry[] = []; + __resetLogRecordEmitterForTests(); + const unsubscribe = __subscribeLogRecordEmitter((entry) => { + records.push(entry); + }); + try { + acquireCSSGenerationSession(true); + acquireCSSGenerationSession(true); + acquireCSSGenerationSession(true); + } finally { + unsubscribe(); + __resetLogRecordEmitterForTests(); + } + + const reports = records.filter((entry) => entry.message.includes("CSSOptimizationEngine")); + assertEquals(reports.length, 1); + assertEquals(reports[0]?.level, "warn"); + assertEquals(reports[0]?.message.includes("@veryfront/ext-css-lightning"), true); + }); + it("keeps minified and unminified output in separate cache identities", async () => { // The reversal above is only safe because an absent optimizer is recorded // in the pipeline identity, so an unminified entry can never be served in diff --git a/src/html/styles-builder/tailwind-compiler.ts b/src/html/styles-builder/tailwind-compiler.ts index bc18becc56..3964895a73 100644 --- a/src/html/styles-builder/tailwind-compiler.ts +++ b/src/html/styles-builder/tailwind-compiler.ts @@ -7,6 +7,7 @@ */ import { tryResolve } from "#veryfront/extensions/contracts.ts"; +import { getRecommendation } from "#veryfront/extensions/recommendations.ts"; import { captureCSSOptimizationEngine, type CSSOptimizationEngine, @@ -70,6 +71,7 @@ const weakSetAdd = WeakSet.prototype.add; const weakSetHas = WeakSet.prototype.has; const CSS_PIPELINE_IDENTITY_SCHEMA = "veryfront.css-pipeline.v2"; const cssGenerationSessions = new WeakSet(); +let reportedMissingOptimizationEngine = false; const inFlightProjectCSS = new Map< string, Promise<{ css: string; hash: string; fromCache: boolean }> @@ -137,11 +139,25 @@ export function acquireCSSGenerationSession(minify: boolean): CSSGenerationSessi // an engine the CSS is emitted unminified and the identity below records it // as such, so no cache can serve a stale minified entry in its place. const optimizationProvider = minify ? tryResolve(CSSOptimizationEngineName) : undefined; - if (minify && optimizationProvider === undefined) { + if (optimizationProvider !== undefined) { + // Re-arm: an engine that disappears later is a new regression to report. + reportedMissingOptimizationEngine = false; + } else if (minify && !reportedMissingOptimizationEngine) { // Warn, not debug: this is a silent quality regression for a project that // did select an optimizer and whose registration failed. It must not be // indistinguishable from a project that never wanted one. - logger.warn("No CSSOptimizationEngine registered; emitting unminified CSS"); + // + // Once per process, not once per acquisition: regenerateCSSByHash acquires + // a session on every cold-cache request, which made this the single most + // frequent line in a hosted project's logs. Name the package that registers + // an engine, or the message asks for a hook with no way to reach it. + reportedMissingOptimizationEngine = true; + const recommendation = getRecommendation(CSSOptimizationEngineName); + logger.warn( + recommendation === undefined + ? "No CSSOptimizationEngine registered; emitting unminified CSS" + : `No CSSOptimizationEngine registered; emitting unminified CSS. Install one with: deno add ${recommendation}`, + ); } const optimizationEngine = optimizationProvider === undefined ? undefined