Skip to content

feat(scripts): generate typed JS API contract and type-check with tsc --checkJs (#1097, #1098) - #1104

Merged
rumblefrog merged 4 commits into
mainfrom
js-typing-and-api-contract
May 4, 2026
Merged

feat(scripts): generate typed JS API contract and type-check with tsc --checkJs (#1097, #1098)#1104
rumblefrog merged 4 commits into
mainfrom
js-typing-and-api-contract

Conversation

@rumblefrog

Copy link
Copy Markdown
Member

Closes #1097, closes #1098. Sequenced as one PR because #1098 imports the
Actions / Perms exports produced by #1097 — splitting them would require
two CI passes' worth of churn for no gain.

Summary

#1097 — Generate typed JS API contract from PHP handler registry

  • New web/bin/generate-api-contract.php parses web/api/handlers/_register.php
    for action names + permission masks, reads web/configs/permissions/web.json
    for the canonical ADMIN_* constants, and emits web/scripts/api-contract.js
    with Actions and Perms globals plus @typedefs for every action. Output
    is alphabetically sorted and byte-stable across runs.
  • Wired as composer api-contract and into .github/workflows/api-contract.yml,
    which fails CI if the file is out of date.
  • Every sb.api.call('foo.bar', …) literal in web/scripts/, web/themes/,
    AND web/pages/ is migrated to Actions.PascalName. The hand-rolled
    ADMIN_* block at the top of sourcebans.js is gone; references go
    through Perms.*.

#1098 — Type-check vanilla JS with tsc --checkJs and JSDoc

  • New web/package.json (TypeScript dev-only — nothing ships from
    node_modules/) + web/scripts/tsconfig.json with checkJs, noEmit,
    strict, strictNullChecks.
  • Every .js under web/scripts/ carries // @ts-check. JSDoc annotations
    on sb.* and sb.api.* exported surface; ambient declarations in
    web/scripts/globals.d.ts for the cross-file shapes.
  • New sb.$idRequired(id) helper for "missing element is a programmer
    error" call sites; sb.$id() callers that handle absence narrow with
    if (!el) return;.
  • ./sbpp.sh ts-check runs tsc --noEmit inside the web container; new
    .github/workflows/ts-check.yml runs the same gate in CI on every PR.
  • docker/Dockerfile gains nodejs npm (Debian 12 ships Node 20).
    docker/README.md documents the gate alongside phpstan and test.

Review-cycle fixups (third commit)

A reviewer pass surfaced three nits, all addressed before push:

  • Two missed sb.api.call('...') literals in web/pages/admin.bans.php
    and admin.comms.php migrated to Actions.*.
  • SbAnyEl doc updated to honestly describe the legacy/strict trade-off
    (form members declared REQUIRED to keep call sites compiling, with a
    flagged hazard and a pointer to a typed-helper follow-up).
  • Generated api-contract.js banner now calls out that most Api…Request
    / …Response typedefs are intentional Object placeholders pending
    per-handler docblocks.

Test plan

  • ./sbpp.sh composer api-contract runs cleanly; second run produces
    a byte-identical file (verified with diff /tmp/snapshot api-contract.js).
  • ./sbpp.sh phpstan[OK] No errors.
  • ./sbpp.sh testOK (37 tests, 99 assertions).
  • ./sbpp.sh ts-check — exit 0, zero diagnostics.
  • rg "sb\.api\.call\(\s*['\"]" web/ — no matches anywhere in web/.
  • rg "^\s*(const|var|let)\s+ADMIN_" web/scripts/ — no matches.
  • All web/scripts/*.js carry // @ts-check.
  • node_modules/ ignored, not tracked.
  • Manual smoke-test in CI's preview / reviewer's local stack: log in
    as admin, exercise an admin-page action that round-trips through
    sb.api.call, confirm no console errors.

Known gaps a reviewer should look at

  • 49 of 56 generated typedefs are still Object placeholders. Tightening
    them is gated on adding @param/@return docblocks to the handler
    bodies — left as a follow-up so this PR doesn't sprawl.
  • SbApiEnvelope.data is typed any for the same reason; converting it
    to a discriminated union keyed off Actions.* is the natural follow-up
    once handler docblocks land.
  • SbAnyEl is intentionally permissive (see the doc comment). New code
    should prefer typed selectors; a follow-up issue could introduce
    sb.$input() / sb.$select() helpers and migrate call sites.

rumblefrog added 3 commits May 3, 2026 17:42
The browser side used to hand-duplicate every action name and the full
ADMIN_* permission table from PHP, which made silent drift (a renamed
handler / a new permission flag) easy and quietly dangerous. This adds a
deterministic generator (`composer api-contract` ->
`web/bin/generate-api-contract.php`) that reads `_register.php` and
`configs/permissions/web.json` and writes `web/scripts/api-contract.js`
with `Actions.*` and `Perms.*` objects, plus best-effort JSDoc typedefs
for handlers that already document their @param/@return shapes. The
panel layouts now load the contract before sourcebans.js, every
sb.api.call('action.name', ...) call site has been migrated to
`sb.api.call(Actions.PascalName, ...)`, and the hand-rolled ADMIN_*
block at the top of sourcebans.js is gone. A new CI workflow runs the
generator on a fresh checkout and fails when the committed file is
stale, so the duplication can never come back.
)

The panel ships plain JS to the browser, but with no compiler in the loop
the JSDoc on sb.* / sourcebans.js was decorative and silent drift was
trivial. This wires up tsc --checkJs over web/scripts as a CI gate so
mistakes — wrong action shapes, null deref on sb.$id, accidental any —
fail before merge. The browser payload is unchanged: tsc only emits
diagnostics, nothing in node_modules ships, and the same `// @ts-check`
+ JSDoc annotations double as IDE hovers.

Adds web/package.json + web/scripts/tsconfig.json (strict, checkJs,
noImplicitAny, strictNullChecks), web/scripts/globals.d.ts to model the
script-tag globals (sb, $, contextMenoo, AddContextMenu, accordion) and
the SbAnyEl / SbApiEnvelope shapes the legacy code reads through, and a
new sb.$idRequired() helper for the common "I just rendered this id"
case. Every existing call site that read .value/.checked off sb.$id()
was migrated to either narrow against null or use $idRequired so the
strict-null pass stays green.

Wires it up end to end:
  - ./sbpp.sh ts-check runs the gate inside the web container, lazy-
    installing TypeScript on first use.
  - docker/Dockerfile gains nodejs + npm so the container has tsc
    without a separate toolchain.
  - .github/workflows/ts-check.yml runs the same gate on every PR
    touching web/** with `npm ci` against the committed lockfile.
  - docker/README.md documents the new gate alongside phpstan/test.

Sequenced after #1097 — Actions.* / Perms.* from the generated contract
are what the type-checker now enforces at every call site.
- Migrate the two remaining `sb.api.call('...')` literal sites in
  `web/pages/admin.bans.php` and `web/pages/admin.comms.php` to
  `Actions.BansAdd` / `Actions.CommsAdd`. The standard header.tpl already
  loads `api-contract.js` first, so `Actions.*` is a global on every page
  reaching these inline scripts.
- Update the `SbAnyEl` doc in `globals.d.ts` to honestly describe the
  trade-off: form members are declared REQUIRED so legacy call sites
  compile, at the cost of letting `sb.$id('div').value` type-check even
  when the runtime value is `undefined`. Comment now flags the bug-hiding
  hazard and points at the typed-helper follow-up.
- Add a banner note to the generated `api-contract.js` (via the
  generator) so readers know most `ApiXxxRequest`/`Response` typedefs are
  intentional `Object` placeholders pending per-handler docblocks.
- Tighten the cross-reference comment at the bottom of `sourcebans.js`
  now that the page-local `ProcessBan()`s actually use `Actions.*`.
@rumblefrog
rumblefrog force-pushed the js-typing-and-api-contract branch from faca49a to 62158eb Compare May 4, 2026 00:30
The two gates added in this PR change the day-to-day dev loop, so the
LLM-facing guide needs to reflect them. Adds the new sbpp.sh subcommands
to the TL;DR, a `tsc --checkJs` blurb to Quality gates alongside the
existing phpstan/test entries, and two Conventions bullets covering the
api-contract.js regen workflow (checked-in like a lockfile, CI fails on
diff, self-hosters never run codegen) and the // @ts-check / SbAnyEl
expectations for JS under web/scripts.
@rumblefrog
rumblefrog added this pull request to the merge queue May 4, 2026
Merged via the queue into main with commit 3294b30 May 4, 2026
4 checks passed
@rumblefrog
rumblefrog deleted the js-typing-and-api-contract branch May 4, 2026 00:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Type-check vanilla JS with tsc --checkJs and JSDoc Generate typed JS API contract from PHP handler registry

1 participant