Skip to content

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.

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.

Terminal window
pnpm add -D @madenowhere/phaze-tsplugin

Then 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.json reloads) take effect on the next TS-server restart.
  • JetBrains IDEs — same tsconfig.json setup; the IDE’s TypeScript service picks it up.
  • Neovim / other editors using tsserver — works as long as the editor uses your repo’s typescript install (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.

TS codeWhat it meansPhaze 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.

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:.

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.