Skip to content

rules editor: TypeScript support with in-browser language service - #1202

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

rules editor: TypeScript support with in-browser language service#1202
evgeny-boger wants to merge 17 commits into
masterfrom
rules-editor-typescript

Conversation

@evgeny-boger

Copy link
Copy Markdown
Member

TypeScript support in the rules editor

Companion to wirenboard/wb-rules#221 (engine-side TypeScript rule support). With that engine installed, .ts files in /etc/wb-rules/ transpile and run natively — this PR makes the web editor a first-class place to write them.

What the user gets

  • .ts rule files can be created, renamed, copied and edited (extension preserved everywhere; syntax highlighting switches to TypeScript mode).
  • Validation in the editor: a browser-side TypeScript language service checks as you type — squiggles with messages, hover shows inferred types, completions are type-aware. It is seeded with the vendored wb-rules.d.ts builtin declarations, so defineRule, dev, PersistentStorage, timers etc. all carry real signatures.
  • Checker parity with the controller: same configuration as the engine-side background check (esnext libs, non-strict), so what the editor flags is what the engine logs. The engine additionally publishes its check results as retained JSON on /wbrules/ts-check/<file> for future in-UI display of the authoritative post-save verdict.
  • Zero cost for .js users: the language service (typescript ≈975 kB gzip + lib texts ≈74 kB gzip) lives in a lazy chunk loaded only when a .ts file is opened. The rule-page chunk stays at 16 kB.
  • Static completion list for .js files is now generated from wb-rules.d.ts (npm run generate:completions), so the two stay in sync.

Implementation notes

  • @valtown/codemirror-ts + @typescript/vfs drive CodeMirror integration; the virtual FS bundles lib.es*.d.ts/lib.decorators*.d.ts via import.meta.glob (no CDN — controllers are offline).
  • One shared language-service environment per file path, recreated on path change (ts-language-service.ts); tsSync() tracks edits after the initial seed.
  • Verification: tsc --noEmit clean, eslint clean, 2418 vitest tests green (2 new for the language service, 3 for .ts file handling in the rules store).

🤖 Generated with Claude Code

https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru

.ts rule files get full editor support:

- Syntax: @codemirror/lang-javascript in typescript mode for .ts files;
  rules-store accepts/preserves the .ts extension (create, rename, copy).
- Validation in the editor: a browser-side TypeScript language service
  (typescript + @typescript/vfs + @valtown/codemirror-ts) checks as you
  type - squiggles, hover type info, type-aware completions - seeded
  with the vendored wb-rules.d.ts builtin declarations. Same settings as
  the engine-side tsgo check (esnext libs, non-strict), so editor and
  controller agree.
- Cost: everything heavy lives in a lazy chunk (~1 MB gzip: typescript
  975 kB + lib.*.d.ts texts 74 kB) loaded only when a .ts file is
  opened; the rule page chunk stays at 16 kB and .js-only users
  download nothing new.
- Static completions for .js files are generated from wb-rules.d.ts by
  scripts/generate-wb-rules-completions.mjs (npm run generate:completions).

Engine side (wirenboard/wb-rules#221) additionally publishes tsgo check
results as retained JSON on /wbrules/ts-check/<file> for future
integration and logs them to the rules console.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
@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 83 complexity · 0 duplication

Metric Results
Complexity 83
Duplication 0

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 and others added 9 commits August 14, 2026 13:46
- Show the controller-side type-check verdict: subscribe to the retained
  /wbrules/ts-check/<file> topic while editing a .ts file and render each
  diagnostic as a page alert (danger/warn by severity, i18n en+ru). The
  in-editor language service stays the live check; this banner is the
  authoritative post-save result from the controller's own tsgo.

Review findings fixed (adversarially verified multi-agent review):
- language-service cache: reseed when the same file is reopened with
  different content (tsSync only tracks in-editor edits), and drop the
  cached promise on load failure instead of poisoning TS support forever
- completion: mergeSources awaits each source, so static completions
  still answer when the async TS source resolves to null
- unsaved .ts rules use a stable placeholder path - title keystrokes no
  longer rebuild the language service per key

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
React escapes on render; i18next's own interpolation escaping showed
&#39; instead of quotes in the banner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
The controller's tsgo diagnostics (retained /wbrules/ts-check JSON) now
render as regular lint entries at the reported lines - squiggles and
gutter markers merged with the local language service's own - with the
tooltip labeled 'controller (tsgo)' so the two checks stay
distinguishable. A mobx autorun re-triggers linting when new MQTT data
arrives. The page-top banner (and its i18n strings) is gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
…heck

Both checkers usually flag the same line with the same message, which
doubled every squiggle. Controller entries matching a local language
service diagnostic (same line, same message) are now dropped; only
skew-only findings - the controller's unique value - render separately.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
…s from Editor.GetTypes

Replaces the retained /wbrules/ts-check MQTT subscription (topic removed
engine-side; it had unsolvable retained-state lifecycle issues). The
controller verdict is now pulled on .ts file open and after each save.

The language service is seeded with the CONTROLLER's installed
wb-rules.d.ts (Editor.GetTypes) when reachable, so the editor validates
against the API of the engine it is actually talking to; the vendored
copy remains as offline fallback. This eliminates declaration version
skew between UI and engine releases.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
User-reported bug: fix an error in the editor and the controller's old
squiggle for that line stayed until the next save. Mechanism: while the
error existed the controller entry was hidden behind the identical local
one; fixing the line removed the local diagnostic and the stale
controller entry (describing the last-saved file) surfaced.

The verdict now carries the editor content it was computed for and
renders only while the document still matches it - the local language
service owns the screen while typing, and saving triggers a fresh
verdict. Covered by controllerDiagsForDoc tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
- Check replies 'pending' while the controller's background check runs:
  poll briefly (700ms x 15), with a token so a newer check supersedes an
  in-flight poll loop. 'not-ts' (e.g. disabled files) and 'unsupported'
  clear the verdict instead of masquerading as clean.
- De-dup by message prefix: the controller carries only the head line
  of chained diagnostics while the local service flattens the whole
  chain, so equality matching let every elaborated error double-squiggle.
- Skip diagnostics that belong to another file (import/reference) - the
  reply now identifies them; anchoring them in the open file was wrong.
- Fix stale transport comment (retained MQTT -> Check RPC).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
- H1/H2: the vendored wb-rules.d.ts and the completion list generated
  from it are excluded from eslint and tsconfig - the ambient engine
  globals (dev, log, require, ...) no longer leak into the whole app's
  type space (a typo'd log() call typechecked before), and the full-repo
  lint is clean again (0 errors, 0 warnings).
- H3: an empty completion result no longer shadows later sources - the
  TS service answers with zero entries inside dev["..."], where the
  device-list source has the real completions; merged sources now fall
  through past empty results.
- M1: snippets and generated globals are one static source (snippet
  variants first) - the generated signatures were unreachable behind the
  snippet source's catch-all match.
- M2: the poll-timeout clear is token-guarded (a stale loop could wipe
  a fresh verdict).
- M3: editor extensions are memoized on [isTypeScript, tsSupport] -
  rebuilding them per keystroke reconfigured CodeMirror and re-ran every
  lint source synchronously per character.
- M4: the completions generator emits repo-style single quotes;
  regeneration is byte-idempotent against the committed file.
- L1: controller types race a 3s deadline so firmware without
  Editor.GetTypes does not stall TS support for the 60s RPC timeout.
- L2: post-save checks capture the exact saved content.
- L3: leaving the page cancels the verdict poll loop.
- L5: ControllerVerdict lives in types.ts per repo convention.
- L7: new deps exact-pinned; typescript moved to runtime dependencies
  (dynamically imported in production); renaming a .ts rule to an
  extensionless title keeps .ts instead of silently becoming .js.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
Plain-JS rule files now get the same in-browser language service as .ts
(allowJs): completions and hover reflect the controller's installed API
via Editor.GetTypes instead of the build-time snapshot, plus live syntax
checking. checkJs stays off, so wild ES5 gets no type-error noise. The
generated static list remains as fallback when the service is
unavailable (offline, pre-GetTypes firmware). Cost: .js-only users now
lazy-load the language-service chunk on first editor open.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
evgeny-boger and others added 7 commits August 14, 2026 21:37
The first environment build parses every bundled lib.*.d.ts - ~2s
locally but 7s in a loaded sbuild chroot, past vitest's 5s default
(seen on Jenkins PR-1202 #15). 30s budget for the two cold-start
candidates.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
(The PR pipeline's version-bump check requires it; also gives the
experimental debs a proper feature version.)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
- 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
…nd, delay(), nextMqtt()

Sync the vendored wb-rules declarations with the engine's new
promise-native library: spawn()/runShellCommand() return
Promise<SpawnResult> (nonzero exit resolves; rejection only when the
process cannot start), delay(ms) is the async setTimeout, and
nextMqtt(topic[, timeoutMs]) resolves with the next live MQTT message
(MqttMessage gains retained/qos, matching what trackMqtt callbacks
always received). Completions regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
Matches the engine-side rename; completions regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
Bump the wb-mqtt-homeui Recommends floor from wb-rules 2.37.0 to
2.47.0~quickjs3 - the engine generation this editor's Editor.Check /
Editor.GetTypes integration and .ts file support target. Recommends
(not Depends) stays correct: with an older engine the editor degrades
gracefully (vendored type fallback, bounded verdict polling), and old
homeui keeps working against the new engine.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AoTGMFABCCENSPaHThxAru
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.

1 participant