Phaze TS plugin
@madenowhere/phaze-tsplugin is a TypeScript language-service plugin that teaches the editor about Phaze’s JSX use:directive attributes. It runs inside your editor’s TypeScript server. It does not transform code, does not run at build time, does not affect runtime.
What it solves
Section titled “What it solves”Phaze’s phaze-compile rewrites <el use:foo={value}> into a post-creation IIFE that calls foo(__el, () => value). The rewrite is AST-only — by the time the runtime sees the code, foo is a regular function call.
The IDE doesn’t see the post-rewrite form. It sees your source: import { foo } from '...' plus a use:foo JSX attribute. TypeScript’s source-level reference tracker counts call sites, JSXIdentifier references, and a few other forms — but JSX namespace attributes are not counted as references. The result: TypeScript flags the import as TS6133 (“‘foo’ is declared but its value is never read”), the editor shows it faded / wavy, and noUnusedLocals: true builds fail.
@madenowhere/phaze-tsplugin wraps getSemanticDiagnostics and getSuggestionDiagnostics on the TypeScript language service and filters out TS6133 / TS6192 / TS6196 diagnostics whose unused name matches a binding the file consumes via a use:NAME JSX attribute. Other unused-import warnings still surface — only the false-positive directive case is suppressed.
Installation
Section titled “Installation”pnpm add -D @madenowhere/phaze-tspluginThen enable it in your tsconfig.json:
{ "compilerOptions": { "plugins": [{ "name": "@madenowhere/phaze-tsplugin" }] }}Editor pickup:
- VS Code — picks up the plugin automatically via its bundled TypeScript language server. Run “TypeScript: Restart TS Server” from the command palette the first time. After that, edits to the plugin (or
tsconfig.jsonreloads) take effect on the next TS-server restart. - JetBrains IDEs — same
tsconfig.jsonsetup; the IDE’s TypeScript service picks it up. - Neovim / other editors using
tsserver— works as long as the editor uses your repo’stypescriptinstall (not a bundled fallback).
If you use noUnusedLocals: true in CI / tsc --noEmit, the plugin doesn’t apply at the command-line — tsc doesn’t load language-service plugins. CI builds that need the same suppression should either omit noUnusedLocals for the affected files or use // @ts-expect-error comments. In practice, app code passes CI cleanly because the import IS used at the JSX level; only the editor’s reference tracker doesn’t see it. The plugin’s role is purely UX.
Diagnostic codes filtered
Section titled “Diagnostic codes filtered”| TS code | What it means | Phaze case |
|---|---|---|
TS6133 | “‘X’ is declared but its value is never read” | Most common — import used only via use:X |
TS6192 | ”All imports in import declaration are unused” | An import block that only contains use:X-consumed names |
TS6196 | “‘X’ is declared but only used in type position (rarer)“ | Less common but the namespace augmentation can trigger it |
Only diagnostics for names matching use:NAME attributes in the same file get filtered. Genuinely-unused bindings still surface.
What it does NOT do
Section titled “What it does NOT do”This plugin is intentionally scoped to one thing. It does not:
- Transform or rewrite source code (that’s phaze-compile)
- Run during your build pipeline (Vite / esbuild / Astro never see it)
- Affect your shipped bundle in any way
- Modify the runtime
- Add JSX-type augmentations (those live in
@madenowhere/phaze’s types and in directive packages’ ambient declarations)
If you remove the plugin from tsconfig.json, builds still work — you just see TS6133 / TS6192 false positives in the editor and on tsc --noEmit for files that only consume directive imports via use:.
Source
Section titled “Source”packages/phaze-tsplugin/src/index.ts is the entire plugin — under 200 lines. The diagnostic-filter walk is straightforward: collect the set of JSXNamespacedName identifiers in the file whose namespace is use, then drop any unused-binding diagnostic whose target name is in that set.