Skip to content

rules editor: TypeScript support - #1202

Open
evgeny-boger wants to merge 11 commits into
masterfrom
rules-editor-typescript
Open

rules editor: TypeScript support#1202
evgeny-boger wants to merge 11 commits into
masterfrom
rules-editor-typescript

Conversation

@evgeny-boger

@evgeny-boger evgeny-boger commented Aug 14, 2026

Copy link
Copy Markdown
Member

TypeScript-aware rules editor, paired with the TypeScript-capable wb-rules (wirenboard/wb-rules#223, #224). Four commits after four review rounds (history squashed; every commit passes tsc and the full 2585-test suite): the language service, the diagnostics, the store and page wiring, packaging. Size note: exceeds the 250-300-line guideline - the language service, its diagnostics sources and their tests form one reviewable unit.

  1. rules editor: TypeScript support with an in-browser language service — error squiggles while typing, hover types and type-aware completions for .ts (and .js via allowJs), seeded with the controller's installed wb-rules declarations (Editor.GetTypes, vendored fallback). The controller's own background verdict (Editor.Check) renders inline, de-duplicated against the local check and suppressed once the document diverges from the checked content.
  2. rules editor: typed async API and registry-typed device references — the promise-native API and changed() in the declarations; dev["dev/ctrl"] / getControl("dev/ctrl") / changed() typed against an interface WbControls registry generated from the live devices store — the same mechanism the controller check uses.
  3. rules editor: warn on forgotten await (Promise misuse) — four custom diagnostics tsc doesn't provide: Promise as a condition, floating Promise in an infinite loop, await of a non-Promise, Promise written to a control.
  4. rules editor: check .js too, runtime errors inline, and diagnostics that do not depend on hovering.js checked with the controller-matching advisory policy; runtime errors the engine attributes to a line (control x/y: write ignored (…) at file:line, uncaught exceptions, watchdog aborts) become error squiggles on the open rule while it matches what runs; and every diagnostic is legible without hovering: inline error-lens messages, lint gutter markers, a Problems panel with a header badge (F8 / Ctrl-Shift-M). Includes a fix for a latent CodeMirror pitfall: forceLinting() is a no-op unless a pass is queued, so external diagnostics (the controller verdict too) never appeared until the next keystroke.

Verification

  • Each commit typechecks; the tip passes all 199 test files / 2520 tests, ESLint-clean on the touched files.
  • Verified in a real browser against a WB8 controller: registry-typed completions and squiggles, .js checkJs flagging dev["buzzer/enabled"] = 123, runtime-error squiggle appearing on an idle editor when the rule misfires (with repeat counting and no tooltip flicker), problems badge/panel.
  • Reviewed four times from scratch; all actionable findings fixed in-line.

After the reviews (rounds 3-4)

  • Bundle: the app entry no longer drags the CodeMirror chunk into every page's first load (-170 KB gzip vs the earlier tip; entry is back to master size +0.5 KB). The TS chunk stays lazy and is not downloaded at all on firmwares without Editor.GetTypes.
  • Old firmware: no Editor.GetTypes - no language service (plain editor as before); the vendored d.ts remains only as a fallback for a transient GetTypes failure on advertising firmware.
  • Correctness: external-reload ordering keeps the just-logged load error and re-anchors runningContent via Editor.Load; CRLF files seed the language service LF-normalized; runtime-error locations parse paths with spaces/parentheses/unicode; the editor extension stack is identity-stable (no per-keystroke reconfiguration); per-target RPC method cache; overlapping saves are counted and a failed save restores the errors it cleared.
  • Rebased onto current master (the changelog stanza is now 2.247.0); debian/control Recommends wb-rules (>= 2.47.0~quickjs2~~); project-rules conformance (types in types.ts, index exports, new tests in new files).

🤖 Generated with Claude Code

https://claude.ai/code/session_01BvpkTQYSS1c78EpBCpLTpB

223-tla-console-errors 224-ts-check-console 1202-editor-diagnostics 1202-problems-panel 1202-registry-completions 1202-runtime-error-inline

@evgeny-boger
evgeny-boger requested a review from a team as a code owner August 14, 2026 10:52
@codacy-production

codacy-production Bot commented Aug 14, 2026

Copy link
Copy Markdown

Up to standards ✅

🟢 Issues 0 issues

Results:
0 new issues

View in Codacy

🟢 Metrics 298 complexity · 6 duplication

Metric Results
Complexity 298
Duplication 6

View in Codacy

NEW Get contextual insights on your PRs based on Codacy's metrics, along with PR and Jira context, without leaving GitHub. Enable AI reviewer
TIP This summary will be updated as you push new changes.

evgeny-boger added a commit that referenced this pull request Aug 15, 2026
- H1: type-aware completions actually surface the wb-rules API now.
  valtown's completion filter drops every ambient global (sortText "15")
  not on its hardcoded standard-JS whitelist, so defineRule & co never
  appeared once the language service loaded - and its non-empty answer
  shadowed the static sources, a regression for .js files too. Pass
  keepLegacyLimitationForAutocompletionSymbols: false and merge the
  snippet templates into the service's answers (snippets replace the
  plain entry of the same label; member accesses stay service-only).
- M1: device/topic completions read the devices store at completion
  time instead of being snapshotted when the (memoized) extension array
  is built - devices arriving over MQTT after page load now show up in
  dev["..."], getDevice(...), publish(...) lists.
- M2: the rename uniqueness pre-check now tests the path the rename
  will actually target (a .ts rule keeps .ts; only a fresh save
  defaults to .js) - renaming foo.ts to an occupied extensionless name
  no longer slips past the check and silently fails in the engine.
- M3: typing the title of an unsaved rule no longer re-runs the
  language-service effect per keystroke (each run cost an
  Editor.GetTypes RPC); the effect is keyed on the stable service path.
- L1: controller verdict diagnostics survive CRLF rule files - compare
  the checked content LF-normalized, matching CodeMirror's own
  normalization on ingest.
- stale comments updated (the service runs for .js files too).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
@evgeny-boger
evgeny-boger force-pushed the rules-editor-typescript branch from f1f34a3 to 79bd635 Compare August 19, 2026 15:17
@evgeny-boger evgeny-boger changed the title rules editor: TypeScript support with in-browser language service rules editor: TypeScript support Aug 19, 2026
@evgeny-boger
evgeny-boger force-pushed the rules-editor-typescript branch 2 times, most recently from b22ba95 to 4055bb9 Compare August 20, 2026 14:57

@evgeny-boger evgeny-boger left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

смотрел, но плохо

@evgeny-boger
evgeny-boger force-pushed the rules-editor-typescript branch 3 times, most recently from 1fdf1db to 853f874 Compare August 21, 2026 09:24

@ninedev-i ninedev-i left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Все круто, работает, нравится.

Бесят огромные, в 80% ненужные комментарии в коде

Comment thread debian/control Outdated
evgeny-boger and others added 4 commits August 24, 2026 17:41
…etions

A TypeScript language service runs in the browser for rule files,
built on a virtual FS seeded with the wb-rules API declarations
(vendored wb-rules.d.ts, generated from the engine; a later commit
lets the controller supply its own copy). Seeding and every
incremental update are LF-normalized: CodeMirror documents are
LF-only, and a stray CRLF in stored file content would shift every
diagnostic and completion position after it (pinned by the CRLF
test).

Completions are type-aware and merged with the static sources: the
device/topic string contexts (dev["...], getDevice(...)) answer from
the live device list first, then the language service, then snippets
and the generated global signatures as the no-service fallback. An
empty service result must not shadow later sources - except inside a
string literal, where junk globals are worse than an empty popup.
Member accesses and string-literal completions stay the service's
alone. buildControlsRegistry turns the device list into declarations
so dev["device/control"], getControl(...) and changed(...) know each
control's value type and complete the existing names.

The diagnostics linter filters and rephrases the raw TS output for
rule authors, including the Promise-misuse family: a promise used as
a condition, a forgotten await in an endless loop, await of a
non-promise.

globals-generated.ts is produced by scripts/generate-wb-rules-
completions.mjs from wb-rules.d.ts; a drift test regenerates it
through the script and diffs the result against the committed file
byte for byte. The generated file and the vendored declarations are
excluded from eslint and from the app tsconfig. The typescript
package moves to dependencies (it now ships in a lazily imported
editor chunk); TsModule is passed around as a type-only import so
the heavy package stays out of everything the entry pulls in.

stores/rules/types.ts gains the shared wire types (TsCheckResult,
TsCheckDiag, LocalTsDiag, RuleRuntimeError) that the service, the
diagnostics UI and the store wiring build on in the follow-up
commits.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvpkTQYSS1c78EpBCpLTpB
…, gutter, problems panel, runtime errors

Squiggles that only speak on hover are useless on a controller
touchscreen. diagnostics-ux.ts renders every diagnostic as a short
message after the offending line (an error lens), a marker in the
lint gutter, and a problems counter that opens the panel (F8 jumps
to the next problem). The load error of the file joins the same
gutter as one more lint diagnostic - one marker column, not two
competing ones.

The diagnostic sources that feed it are mobx-reactive linters:
controller-diagnostics renders the controller's own check verdict
de-duplicated against the local language service (same line and
message means the local squiggle already covers it), and held back
while the buffer has unsaved edits - a verdict for old content must
not mark new lines. load-error anchors the engine's in-band
Save/Load error. runtime-errors renders errors from the rules
console (a rejected control write, an exception with a file:line)
inline at the line the engine attributes them to; repeats of the
same error at the same place are counted, not duplicated.
runtime-error-parse extracts those locations from the console
messages - paths survive spaces, parentheses and unicode because a
path is everything up to the first .js/.ts before the line number,
anchored on the at that precedes every engine-reported location.
lint-refresh is the shared plumbing that forces a lint pass when a
store value changes rather than when the user types.

CodeEditor stops rebuilding its extension stack on every keystroke:
the underlying component reconfigures whenever extensions or
onChange change identity, and pages passing inline handlers
re-render per keystroke. onSave/onChange are bridged through refs
(the extension effect depends on whether a save handler exists, not
on its identity), and useAsyncAction keeps the wrapped action in a
ref so execute has one identity for the component's lifetime while
still calling the latest action. Tests pin the contract: a rerender
with new inline handlers hands CodeMirror the same props, Mod-s
still runs the latest handler, a genuinely new extensions prop still
rebuilds.

These modules are self-contained here; the rules page starts feeding
them in the next commit. Locale keys for the problems counter and
the load error marker come along.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvpkTQYSS1c78EpBCpLTpB
The edit page builds the full editor out of the previous commits.
Editor.GetTypes doubles as the feature gate: firmware that does not
advertise the method gets a plain editor - no language service and
no download of the heavy TypeScript chunk, because a legacy engine
must not get completions and promise semantics it does not have. On
advertising firmware the controller's declarations seed the service,
the vendored copy is solely the fallback for a transient GetTypes
failure, and the chunk import overlaps the GetTypes reply. The
language service is loaded with a dynamic import, and the store side
reaches it only through type-only imports - the app entry bundle
stays free of CodeMirror and TypeScript. A service init resolving
after navigation away is dropped; a reused service is reseeded so a
reopened editor starts from the stored content, not from what was
typed and discarded.

The store polls the controller-side check (Editor.Check, with
backoff and an attempt cap) for .ts and plain .js files alike, and
records runtime errors from the rules console per file and line for
the page to render. Save and external reload keep their ordering
guarantees: the engine loads the new version of a file BEFORE it
publishes /wbrules/updates/changed, so a load-time error of the new
version arrives just ahead of the notification - the changed handler
must clear only the errors of the replaced version, not the fresh
one it just received.

rpcHasMethod caches availability per target AND method: different
services may well share a method name, and one advertisement must
not answer for another target. A method advertised only after the
first ask timed out becomes available for the session instead of
staying "unavailable". rpc-has-method.test.ts pins the cache, the
retained advertisement delivered synchronously at subscribe time,
and the late advertisement; the page tests cover the GetTypes gate
and the fallback; the store tests cover the check polling limits,
the runtime-error recording policy and the reload race.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvpkTQYSS1c78EpBCpLTpB
Changelog entry for 2.247.0 describing the TypeScript rules editor.
The wb-rules recommendation moves to 2.47.0~quickjs2 - the engine
that serves Editor.GetTypes and Editor.Check and runs .ts rules; it
stays Recommends rather than Depends because the editor degrades
gracefully on older firmware (the GetTypes gate in the page).

.codacy.yml excludes frontend/scripts from analysis instead of
inline suppressions: the completions generator is build/dev tooling,
not shipped code, and it writes to a caller-supplied output path by
design (the drift test uses a temp file).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BvpkTQYSS1c78EpBCpLTpB
@evgeny-boger
evgeny-boger force-pushed the rules-editor-typescript branch from 853f874 to bc77f97 Compare August 24, 2026 17:43
…boundaries

The upstream master shipped 2.247.0 (local_time rename) while this branch also claimed 2.247.0, failing the version-bump check on the merge build; the earlier changelog merge had also glued two stanzas without a separating blank line. The branch entry moves to 2.248.0 on top of master's history taken verbatim.
evgeny-boger and others added 6 commits August 25, 2026 19:49
The TS language service has the data (getCompletionEntryDetails,
getSignatureHelpItems) and CodeMirror has the slots (Completion.info /
detail, showTooltip), but the @valtown/codemirror-ts adapter wires
neither: its completions carry only { label, kind }, so the popup showed
just the name (e.g. device.writeChannel with no signature or docs), and
it ships no signature help at all.

ts-help.ts adds both from the same service:
- withCompletionDetails wraps the completion source so each entry gets a
  one-line `detail` (its signature) and an `info` panel (signature +
  JSDoc + @tags), fetched lazily per highlighted entry.
- signatureHelp is a StateField that shows a parameter-hint tooltip for
  the call the cursor is inside, the active argument emphasised.
Wired into ts-language-service.ts (member completions keep the service's
own list, so the details survive withStaticExtras); styles in the
code-editor CSS. Tested against a real language service over a small
d.ts (ts-help.test.ts): the signature and JSDoc reach the panel, the
active parameter is marked, and no call means no tooltip.
The parameter-hint tooltip added in the previous commit showed on every
cursor move and overlapped the autocomplete popup, and its per-keystroke
effect dispatch made the editing feel jumpy. The completion popup already
shows the full signature (getCompletionEntryDetails displayParts) next to
the JSDoc, so the separate tooltip is removed. The completion info is now
built lazily - only for the entry the user highlights - instead of a
getCompletionEntryDetails call per entry on every keystroke, so a long
member list does not lag the popup. Hover (tsHover) still gives the type
under the pointer.
The error lens appends the message inline at the end of the line. On the
line you are typing, that widget sits right next to the caret, so as the
diagnostic appears the caret looks like it jumped to the start of the
message (it does not actually move - verified: pushing diagnostics leaves
the selection put). Matching VS Code, the lens is now suppressed on any
line a cursor is on and reappears the moment the caret leaves it; the
squiggle, the gutter marker and the hover tooltip still flag that line.
The lens plugin recomputes on selection changes too. Test: no lens on the
caret's line, the caret stays, and the lens returns when the caret moves.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UZtLwwQGQLcuL7XsxJicyo
@evgeny-boger

Copy link
Copy Markdown
Member Author

ИИ: Комментарии порезал: в добавленном коде было 620 строк комментариев, стало 240 (2dc05f4, только комментарии, тесты 330/330). Оставил в 1–2 строки только неочевидное — контракты порядка сообщений в MQTT, формат локаций из движка, инварианты кэша TS. Плюс влил master (2.250.0 поверх вышедшей 2.249.0) — PR clean.

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.

2 participants