Skip to content

fix: dogfood triage (route() middleware, scaffold typecheck, scaffold a11y)#879

Merged
vivek7405 merged 8 commits into
mainfrom
fix/middleware-typecheck-a11y
Jul 9, 2026
Merged

fix: dogfood triage (route() middleware, scaffold typecheck, scaffold a11y)#879
vivek7405 merged 8 commits into
mainfrom
fix/middleware-typecheck-a11y

Conversation

@vivek7405

@vivek7405 vivek7405 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Fixes three dogfood findings from cross-checking the blog claims against a real app. Bundled into one PR per request.

Closes #876
Closes #877
Closes #878

#876 route() adapter ignores an action's declared middleware

route(action) only ran middleware/validate passed via opts, so an action's declared export const middleware protected the RPC boundary but not the REST one (a function reference cannot reach its sibling config exports). route() now also accepts the action's MODULE NAMESPACE (import * as m; export const POST = route(m)) and auto-applies the declared middleware + validate, so a guard declared once beside the action protects both boundaries. The function form is unchanged; an explicit opts value still overrides the declared config.

  • packages/server/src/action-route.js: resolve the callable + declared config from a function or a module namespace.
  • Tests: packages/server/test/actions/action-route.test.js (module form applies middleware + validate, opts override, multi-function throw, and the counterfactual that the bare-function form does NOT apply declared middleware).
  • Docs: corrected the AGENTS.md / agent-docs/recipes.md / docs-site overclaim to match, and documented the new module form.

#877 fresh saas scaffold fails webjs typecheck

  • The saas generator rewrote the registry cn() import but not the sibling onBeforeCache one, so dialog.ts kept ../lib/dom.ts (a nonexistent components/lib/dom.ts) and failed TS2307. Added the ../lib/dom.ts -> #lib/utils/dom.ts rewrite in saas-template.js, mirroring create.js.
  • lib/auth.server.ts assigned process.env.AUTH_SECRET (string | undefined) to the required string secret (TS2322). Now resolved through a typed authSecret const with a dev fallback that fails fast in production.

#878 fresh saas scaffold a11y violations

  • The login / signup / dashboard / settings pages used <h3> as their only heading (axe page-has-heading-one). Promoted each page title to <h1> (styling unchanged).
  • The counter-card demo label used text-muted-foreground/70 (3.83:1, below AA). Dropped the /70.

Verification

  • Regenerated a fresh saas app with this branch's CLI: webjs typecheck clean (0 errors); axe returns 0 violations on /login, /signup, and /features/components in both light and dark themes; exactly one <h1> per page.
  • packages/server/test/actions/action-route.test.js: 18/18 (module form + counterfactual).
  • test/scaffolds/scaffold-integration.test.js + scaffold-ui-integration.test.js: 13/13, with new assertions doubling as counterfactuals (verified: reverting a fix reds the test).

Definition-of-done notes

…idate

The route() adapter only ran middleware/validate passed via opts, so an
action's declared `export const middleware` did not protect the REST
boundary, only the RPC one, despite the docs claiming both. A function
reference cannot reach its sibling config exports.

route() now also accepts the action's module namespace (`import * as m`);
given a module it resolves the single action function and auto-applies
the declared middleware and validate, so a guard declared once beside the
action protects the RPC and REST boundaries alike. The function form is
unchanged and an explicit opts value still overrides the declared config.

Corrects the AGENTS.md / agent-docs / docs-site overclaim to match.

Closes #876
@vivek7405 vivek7405 self-assigned this Jul 9, 2026
vivek7405 added 3 commits July 9, 2026 18:10
Two dogfood findings on a freshly generated saas app.

Typecheck (#877):
- The saas generator rewrote the registry cn() import but not the sibling
  onBeforeCache import, so dialog.ts kept the registry-relative
  ../lib/dom.ts (a nonexistent components/lib/dom.ts) and failed TS2307.
  Add the ../lib/dom.ts -> #lib/utils/dom.ts rewrite, mirroring create.js.
- lib/auth.server.ts assigned process.env.AUTH_SECRET (string | undefined)
  to the required string secret (TS2322). Resolve it through a typed
  authSecret const with a dev fallback that fails fast in production.

Accessibility (#878):
- The login / signup / dashboard / settings pages used <h3> as their only
  heading, so axe flagged page-has-heading-one. Promote each page's title
  to <h1> (styling unchanged, cardTitleClass carries the size).
- The counter-card demo label used text-muted-foreground/70, a 3.83:1
  contrast against the card, below AA. Drop the /70 opacity.

A fresh `webjs create --template saas` now typechecks clean and returns
zero axe violations on /login and the feature gallery, in light and dark.

Closes #877
Closes #878
Extend the saas scaffold integration test with the regressions #877/#878
fix: the onBeforeCache import is rewritten to #lib/utils/dom.ts, the auth
secret is a typed const (not the raw string | undefined env read), the
login/signup pages carry an <h1>, and the counter-card label keeps full
text-muted-foreground contrast. Each doubles as the counterfactual that
fails when the corresponding fix is reverted.
Self-review of the h1/contrast fix surfaced three gaps a fuller axe
sweep confirmed:
- The dashboard and settings pages already carry a page <h1>, so
  promoting their card title to <h1> created a second h1. Demote those
  two card titles to <h2> (login/signup keep <h1>, their card title is
  the sole heading).
- text-muted-foreground/70 (3.83:1) appeared in nine more gallery
  surfaces than the counter-card; drop the /70 across the gallery so no
  generated page ships sub-AA label text.
- The file-upload input and the ref-focus demo input had no accessible
  name (axe label, critical); add aria-labels.

A full axe sweep of every generated route (public and auth-gated) now
returns zero violations, with exactly one h1 per page. Tests widened to
assert one h1 on dashboard/settings, scan the whole gallery for the
opacity token, and pin both aria-labels.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Went back over the scaffold a11y change with fresh eyes and the /login + gallery axe run I did was too narrow. Two things it missed:

  1. The dashboard and settings pages already have a page-level h1, so promoting their card title to h1 double-heads them. Those two should be h2; only login/signup (where the card title is the only heading) are correctly h1.
  2. The /70 muted token was not just on the counter-card. A full sweep found it on nine more gallery surfaces, so the contrast fix was half-done.

Both are fixed now, and I widened the sweep to every generated route (public and auth-gated) plus the tests. Details on the two lines below.

Comment thread packages/cli/lib/saas-template.js
Second self-review round:
- The production AUTH_SECRET guard used `!process.env.AUTH_SECRET`, which
  a whitespace-only value passes, so a blank secret would sign tokens in
  production. Trim first and fail fast on empty/blank; the dev fallback
  now also triggers for a blank value.
- The AGENTS.md and recipes.md #876 examples reused the `createPost`
  identifier for both the module-namespace binding and the bare-function
  form, so the same `route(createPost)` call read as doing two opposite
  things. Use distinct bindings (import * as postActions for the module
  form, import { createPost } for the function form).

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Second pass, zoomed in on the route() code path, the auth guard, and the doc prose. The route() module resolution holds up (single-action rule matches the existing one-action-per-file check, default export works, explicit opts win). Two smaller things worth fixing, both addressed:

Comment thread packages/cli/lib/saas-template.js
Comment thread agent-docs/recipes.md
#876 gives route() an automatic path (pass the module namespace) for
applying an action's declared middleware, so the blog's earlier "route()
does not pick it up on its own, a real seam to remember" framing is now
stale. Rewrite that section, the heading, and the takeaway to show the
module form auto-applies and the bare-function form takes the explicit
option.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Third pass, cross-file. The route() code and the three in-tree doc surfaces (AGENTS.md, recipes.md, the docs site) are consistent, the auth-secret trim is type-safe and gates prod correctly, route(null)/route({}) throw sensibly, and every functional fix has a real (non-tautological) test. One drift item: the per-action-middleware blog still described the old behavior, so I folded the sync into this PR since #876 is what changed it. Noted inline.

Comment thread blog/per-action-middleware.md
Match the recipes.md disambiguation (#876 follow-up): the module-namespace
example used the same createPost identifier as the bare-function form, so
route(createPost) read as doing two opposite things. Use postActions for
the module form.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fourth pass. Blog is internally consistent and the PR body claims all still hold. One leftover: AGENTS.md kept the reused-identifier ambiguity that e9… fixed in recipes.md. Fixed to match.

Comment thread AGENTS.md
The docs-site paragraph described the module form but its code example
(and the blog + JSDoc examples) still reused one identifier for both
forms, so route(createPost) read as auto-applying in prose while the
example showed the non-applying bare-function form. Label the docs-site
code block as the bare-function form and use `import * as postActions`
for the module form across the docs site, the blog, and the JSDoc, to
match AGENTS.md and recipes.md.

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fifth pass, the doc consistency thread. Found a real one on the docs site plus a matching clarity gap in the blog, both fixed. The route() code, tests, and the auth/a11y fixes are unchanged and hold.

Comment thread docs/app/docs/server-actions/page.ts
Comment thread blog/per-action-middleware.md

@vivek7405 vivek7405 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Final pass, the route() doc surfaces now line up: module form is postActions everywhere, the docs-site code block is labeled the bare-function form and matches its prose, no reused-identifier collision left. Nothing else outstanding. Good to merge.

@vivek7405 vivek7405 marked this pull request as ready for review July 9, 2026 13:42
@vivek7405 vivek7405 merged commit 72bb361 into main Jul 9, 2026
10 checks passed
@vivek7405 vivek7405 deleted the fix/middleware-typecheck-a11y branch July 9, 2026 13:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant