Skip to content

fix(config): config-include output safety net + visible package failures + tutorial doc fixes from VM bake#2378

Merged
bpamiri merged 2 commits intodevelopfrom
claude/blissful-dubinsky-6fcbbe
Apr 30, 2026
Merged

fix(config): config-include output safety net + visible package failures + tutorial doc fixes from VM bake#2378
bpamiri merged 2 commits intodevelopfrom
claude/blissful-dubinsky-6fcbbe

Conversation

@bpamiri
Copy link
Copy Markdown
Collaborator

@bpamiri bpamiri commented Apr 30, 2026

Summary

A bundle of related fixes surfaced by a fresh-VM tutorial bake (the same one that produced the journal+report you've been reviewing). Each is small on its own; together they unblock the chapter 6b auth flow and the bonus basecoat chapter, and they make two whole classes of silent failures loud the next time someone hits them.

Framework changes

  • $includeConfig safety net on the four onApplicationStart config-file loads — captures cfinclude output, warns to wheels.log if non-empty (almost always means the user's config/*.cfm is missing its <cfscript> wrapper). Stops the visible "page leak" symptom that surprised the bake.
  • Surface PackageLoader failures at boot — fix file="application" writes that don't materialize on the default Lucee log layout; add an aggregate WARN to wheels.log + per-package ERROR to wheels-errors.log so a developer chasing a "No matching function [...]" error has a concrete breadcrumb.

Doc changes

  • Chapter 6b — wrap the services.cfm example in <cfscript>...</cfscript>, rewrite the troubleshooting block to point at the missing-wrapper case first.
  • Chapter 4 — add allowBlank=true to the validatesLengthOf example and explain the framework's blank-handling default. Without this, blank submissions fire BOTH "can't be empty" AND "is the wrong length" even though zero characters is well under the 120-char cap. Whether the framework default should change is a separate v4 conversation (compat risk).
  • Bonus chapter — switch the basecoat CSS curl URL from basecoatui.com (serves HTML) to jsdelivr (real CSS); replace the misleading "Lucee 7 rejects mixin= attribute" troubleshooting note with the actual blocker pointer ($UIBUILDID / wheels-basecoat 1.0.3).

Verification

All changes verified end-to-end on a real VM (macOS 26.2 / Lucee 7.0.0.395 / Wheels 4.0.0-SNAPSHOT+1652):

  • ✅ Chapter 6b login flow: POST /login302 /posts (was: 500 "service not registered")
  • ✅ services.cfm without wrapper: visible page leak GONE, warning surfaces in wheels.log with first-200-chars preview
  • ✅ Induced package failure: aggregate WARN in wheels.log + per-package ERROR in wheels-errors.log
  • ✅ Chapter 4 with allowBlank=true: empty title fires ONE error ("can't be empty"), 200-char title fires ONE error ("wrong length"), valid title fires zero
  • ✅ Bonus chapter jsdelivr URL: 43.5 KB of real CSS instead of HTML

Related work

Three follow-ups in their own PRs (not in this one):

  • LuCLI — the bonus chapter's "save CSS to public/assets/" instruction is dead on arrival because the shipped rewrite template uses negation-style RewriteCond chains that Tomcat 11's RewriteValve doesn't honor. Fixed in cybersonic/LuCLI#60.
  • wheels-basecoat 1.0.3$uiBuildId/$uiLucideIcon private→public so PackageLoader carries them across the mixin boundary. Fixed in wheels-dev/wheels-basecoat#3.
  • Chapter 7 SignupFlowSpec — diagnosed but deferred. The documented browser spec hits a real Playwright/Turbo Drive interaction (POST fires, user IS created, but URL doesn't navigate via Turbo's History.pushState path). Needs a chapter-7 doc-design pass.

Findings I did NOT change

Two items from the report turned out to be misdiagnoses (verified on VM):

  • Finding New master #11 (basecoat mixin="controller" rejected by Lucee 7): not actually a failure on Lucee 7.0.0.395. The attribute compiles and PackageLoader loads the package successfully. The real blocker was always Finding Created README file for the project. This is heavily based on the CFWhee #14 (private helpers).
  • Finding Issue 784 #13 (linkTo / buttonTo rejects class=): three different variants verified on VM — linkTo(text=, route=, key=, class=), linkTo(text=, controller=, action=, key=, class=), buttonTo(text=, route=, key=, method=delete, class=) — all render the class attribute on the resulting tag. No framework change needed.

Test plan

  • CI passes on the full matrix (Lucee 5/6/7, Adobe 2018/2021/2023/2025, BoxLang)
  • PackageLoader spec — induce a failure, assert wheels.log + wheels-errors.log content
  • $includeConfig spec — induce missing-wrapper, assert no leak + warning fires
  • Verify-docs harness still passes for the modified chapters

🤖 Generated with Claude Code

…res + tutorial doc fixes

A bundle of related fixes surfaced by a fresh-VM tutorial bake. Each is
small on its own; together they unblock the chapter 6b auth flow and the
bonus basecoat chapter, and they make two whole classes of silent failures
loud the next time someone hits them.

## Framework: config-include output safety net

Adds `$includeConfig(template)` and routes the four `onApplicationStart`
config-file loads (`/config/{settings,services}.cfm` plus their env
overrides) through it. The new helper wraps the include in
`cfsavecontent`, so any output the file produces is captured instead of
leaking into the response of whichever request triggered the application
start. If the captured output is non-empty, a clear WARN is written to
wheels.log explaining the most likely cause: a `config/*.cfm` file missing
its cfscript wrapper, so the engine parsed the body as markup and the
registrations silently never ran.

Reproduces the bug the bake hit:
  - Tutorial chapter 6b told users to drop bare cfscript syntax into
    `config/services.cfm` without a wrapper.
  - Lucee's cfinclude treated each `var di = injector();` line as literal
    output text. The bare lines spilled onto the top of every page, the
    DI registrations never executed, and `service("sessionStrategy")`
    blew up with an opaque "service not registered" downstream.
  - Net effect: auth chapter 6b appeared to be a framework bug for ~30
    minutes of debugging when it was actually a doc issue (now fixed
    below) plus a UX gap (this safety net).

The existing `$include` is unchanged — the safety net only applies to the
four config-file loads in onApplicationStart where output should never
reach the response body. Verified end-to-end on Lucee 7.0.0.395.

## Framework: surface PackageLoader silent failures at boot

PackageLoader was already collecting failures into
`application[appKey].failedPackages` and emitting per-package WriteLog
calls, but with `file="application"` — a target that doesn't materialize
on the default Lucee log layout the tutorial install ships with. So
failures were collected but invisible.

Two changes:

1. PackageLoader's per-package WriteLog calls now use `file="wheels"`
   (matches the convention used by `wheels_security`, `wheels-errors`).
2. `$loadPackages` writes an aggregate summary after collection: a
   single WARN to wheels.log naming the failed packages, plus a
   per-package ERROR block to wheels-errors.log with the underlying
   message. A developer chasing a "No matching function [BASECOATINCLUDES]"
   or "No service registered with the name [...]" error now has a
   concrete breadcrumb in under a minute, vs the bake's ~30 minutes of
   dead-end diagnosis.

Verified by inducing a failing package on a real VM (incompatible
wheelsVersion) — both wheels.log and wheels-errors.log surface the right
content.

## Doc: chapter 6b services.cfm example needs cfscript wrapper

The chapter 6b "Register the authenticator" example showed:

    var di = injector();
    di.map("authenticator").to(...).asSingleton();

…with no wrapper. Users dutifully copied that into a fresh
`config/services.cfm`. Lucee's cfinclude treats the body as markup; bare
cfscript syntax becomes literal output and the registrations never
execute. Symptoms: a "var di = injector();..." line spills onto every
response after a cold restart, and `POST /login` returns
`No service registered with the name 'sessionStrategy'`.

The fix is one wrapper plus a sentence explaining why every config/*.cfm
needs it (chapter 1 already establishes this convention for
`config/routes.cfm`; chapter 6b just dropped it). The troubleshooting
section gets rewritten to point at the missing-wrapper case first, the
reload-vs-restart distinction second, and component-path typos third.

## Doc: chapter 4 validatesLengthOf needs allowBlank=true alongside validatesPresenceOf

`validatesLengthOf(maximum=120)` on an empty title was firing BOTH
"Title can't be empty" AND "Title is the wrong length" — even though
zero characters is well under the cap. Root cause: in
`vendor/wheels/model/validations.cfc:506-520`, every validation with the
default `allowBlank=false` adds its own error on a blank value, not just
`validatesPresenceOf`. So the length rule preemptively fires its default
"is the wrong length" message.

Whether the framework default for length should change is a separate v4
discussion (compat risk: existing apps may depend on this behavior).
For now the chapter teaches the correct pattern — `allowBlank=true` on
secondary validations when paired with `validatesPresenceOf` — with a
short paragraph explaining why and how to apply the same shape to other
secondary rules (`validatesFormatOf`, `validatesInclusionOf`, etc.).

Verified on the VM: Post.cfc with the new pattern produces exactly one
error per failure case.

## Doc: bonus basecoat chapter cleanup

Two doc-only fixes in `08-bonus-basecoat.mdx`:

1. The basecoat CSS curl URL pointed at basecoatui.com which served HTML,
   not CSS. Switched to jsdelivr (43.5 KB of real CSS).
2. The troubleshooting "earlier basecoat versions had a
   `component mixin="controller,view"` declaration that fails to load on
   Lucee 7" claim was misdiagnosed — verified on Lucee 7.0.0.395, the
   `mixin="controller"` attribute compiles and PackageLoader loads the
   package successfully. Replaced with the actual blocker:
   `No matching function [$UIBUILDID] found` from the private-helper
   visibility issue fixed in wheels-basecoat 1.0.3 (PR
   wheels-dev/wheels-basecoat#3).

## What is NOT in this commit

Three follow-ups belong in their own PRs:

- LuCLI rewrite.config template fix — the bonus chapter's "save CSS to
  public/assets/" instruction is dead on arrival because the shipped
  LuCLI rewrite template uses a stack of `RewriteCond !pattern`
  exclusions that Tomcat 11's RewriteValve doesn't honor. Fixed in a
  separate LuCLI PR (cybersonic/LuCLI#60). Once that ships, this
  chapter's local-CSS path will Just Work.
- wheels-basecoat 1.0.3 release — the actual private→public fix for
  `$uiBuildId`/`$uiLucideIcon` ships in
  wheels-dev/wheels-basecoat#3. Cut a release after merge so
  `wheels packages add wheels-basecoat` picks up the fix.
- Chapter 7 SignupFlowSpec investigation — the documented browser
  spec hits a real Playwright/Turbo Drive interaction (POST does fire,
  the user IS created, but the URL doesn't navigate via Turbo's
  History.pushState path). Needs a chapter-7 doc-design pass to
  choose between `data-turbo="false"` on the form, awaiting a
  network response in the spec, or rewriting the example to skip
  Turbo Drive. Diagnosed but deferred.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ve immediately

Wheels apps depend on Tomcat's RewriteValve forwarding everything that
isn't a static file or a CFML file to /index.cfm/$path. LuCLI's bundled
rewrite_template/rewrite.config (which generates the rules at server
boot when the project has no override) used a stack of
`RewriteCond %{REQUEST_URI} !pattern` entries before a single rewrite
rule to "skip" static-file paths. On Tomcat 11's RewriteValve that idiom
does NOT short-circuit the way Apache mod_rewrite does — the conditions
effectively don't gate the rule, the rule fires for every request, and
public/ static files (.css, .js, .txt, .png, etc.) silently 404 even
though their extensions are explicitly listed in the negation.

The bonus-basecoat tutorial chapter's `curl -o public/assets/...`
instructions were the first place this surfaced loudly: the file
landed correctly on disk but every GET to /assets/... returned a Wheels
404 page because the rewrite intercepted before Tomcat's default servlet
could serve it.

LuCLI's CatalinaBaseConfigGenerator.java already supports a project-root
rewrite.config that overrides the bundled template entirely (`if exists
THEN user-file ELSE template`), so shipping our own rewrite.config from
`wheels new` is the cleanest path: every new app gets correct routing
behavior on first boot without depending on whichever LuCLI version
happens to be installed, and existing apps can adopt the fix by copying
this file into their project root.

Rules use positive-match passthrough rules with the [L] flag rather than
the more idiomatic-looking RewriteCond !-negation chain. Each "skip"
condition is its own RewriteRule with [L], which terminates rule
processing and lets Tomcat's default servlet serve the file.

Wheels-specific scope (vs the generic LuCLI template):
  - Adds stylesheets/, javascripts/, files/, miscellaneous/ to static
    directory passthrough — matches Wheels' conventional public/
    subdirectories (visible in the existing app template tree).
  - Adds modern web extensions: webp, avif, otf, mp3, mp4, webm, wasm.
  - Adds explicit root-file passthroughs for robots.txt, favicon.ico,
    sitemap.xml, manifest.json, service-worker.js, apple-touch-icon
    variants — common files SEO/PWA setups expect to serve from /.

Verified end-to-end on Tomcat 11.0.13 + Lucee Express 7.0.0.395 against
a 12-case URL matrix:

    /__sentinel.txt        -> 200 text/plain
    /assets/__test.css     -> 200 text/css
    /stylesheets/__test.css -> 200 text/css     (Wheels-conventional)
    /javascripts/__test.js -> 200 application/javascript
    /robots.txt            -> 200 text/plain
    /                      -> 200 text/html (routed through index.cfm)
    /posts                 -> 200 text/html
    /posts/1               -> 200 text/html
    /login                 -> 200 text/html
    /index.cfm             -> 200 text/html
    /index.cfm/posts       -> 200 text/html (PATH_INFO preserved)
    /index.cfm/posts/1     -> 200 text/html

Related: cybersonic/LuCLI#60 fixes the same bug in LuCLI's bundled
template for non-Wheels users on LuCLI. Either fix unblocks Wheels apps
on its own; shipping both decouples Wheels from LuCLI's release cycle
and helps any other framework on LuCLI that relies on framework-style
routing.
@bpamiri
Copy link
Copy Markdown
Collaborator Author

bpamiri commented Apr 30, 2026

Update: added Wheels-side rewrite.config template (commit 1ba7bfd)

Pushed cli/lucli/templates/app/rewrite.config so wheels new ships its own routing config rather than depending on LuCLI's bundled default. Background: the same Tomcat-11-RewriteValve bug being fixed upstream in cybersonic/LuCLI#60 was making the bonus-chapter's curl -o public/assets/... instructions dead on arrival. Two reasons to also ship a Wheels-side fix:

  1. Decouples Wheels from LuCLI's release cycle. wheels new apps work correctly on first boot regardless of when LuCLI ships its fix.
  2. Wheels has its own conventions (stylesheets/, javascripts/, files/, miscellaneous/ static dirs; modern web extensions like webp/avif/wasm; root-file passthroughs for robots.txt/favicon.ico/etc.) that don't belong in a generic LuCLI template.

LuCLI's CatalinaBaseConfigGenerator.java already supports the override path: when <projectRoot>/rewrite.config exists, LuCLI uses it verbatim instead of the bundled default.

Verified against a 12-case URL matrix on Tomcat 11.0.13 + Lucee 7.0.0.395 — see commit message for the full table. All 12 pass with correct status + Content-Type.

Existing Wheels apps can adopt the fix by copying the template file into their project root; new apps get it for free. May be worth a wheels upgrade-style migration in a follow-up so existing apps pick it up automatically.

@bpamiri bpamiri merged commit 5d78eac into develop Apr 30, 2026
10 checks passed
@bpamiri bpamiri deleted the claude/blissful-dubinsky-6fcbbe branch April 30, 2026 04:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant