1. phaze-tsplugin
1. phaze-tsplugin ← editor (TS Language Service) ← you are here2. phaze-compile ← build-time AST rewriting ├── babel-plugin.ts ← the actual Babel plugin (all the visitors live here) └── vite-plugin.ts ← thin wrapper that adapts it for Vite3. phaze-vite ← island HMR + chunking helpers4. phaze-astro ← Astro integration (island model)5. phaze-cloudflare ← native Cloudflare Workers adapter (whole-page)The first tool in the pipeline runs farthest from the user’s eventual deployed app — it lives inside the editor. phaze-tsplugin is a TypeScript Language Service plugin, not a build-time transform. It produces no output bytes, runs no AST passes, and never sees production. Its sole job is to silence one specific category of diagnostic that TypeScript would otherwise (incorrectly) raise in editors that consume Phaze’s JSX use:directive syntax.
For the full reference (install steps, tsconfig.json wiring, diagnostic codes filtered), see the Phaze TS plugin reference. This page covers the why — what problem this tool exists to solve, and why it’s a separate package from babel-plugin.ts in phaze-compiler.
The false positive
Section titled “The false positive”Write a phaze component that uses a use:autofocus directive on an input:
import { autofocus } from '@madenowhere/phaze-directives'import { signal } from '@madenowhere/phaze'
const value = signal('')
<input use:autofocus={true} value={value} />The IDE shows the autofocus import faded or wavy-underlined with the message:
'autofocus' is declared but its value is never read. ts(6133)
If you also have "noUnusedLocals": true in tsconfig.json, tsc --noEmit errors on it.
This is wrong. The import IS used — <input use:autofocus={true} /> is a Phaze JSX namespace attribute that phaze-compile rewrites at build time to autofocus(__el, () => true), which references the imported binding. The problem is the rewrite happens at build time, but TypeScript’s reference tracker analyzes source. From source-level TS’s perspective, use:autofocus is a JSXNamespacedName attribute — not a JSXIdentifier call or property access — and the reference tracker doesn’t traverse into it looking for binding usages.
So TS counts autofocus as imported-and-unused. Hence TS6133.
The fix’s shape
Section titled “The fix’s shape”phaze-tsplugin patches the TypeScript Language Service’s getSemanticDiagnostics and getSuggestionDiagnostics methods. When the LS returns its diagnostic list, the plugin walks the same file’s AST looking for JSXNamespacedName attributes with namespace use, collects the local-identifier names referenced, and drops any TS6133/TS6192/TS6196 diagnostic whose target name appears in that set.
TypeScript Language Service Editor diagnostics panel───────────────────────── ────────────────────────ts.getSemanticDiagnostics(file) │ ▼ [TS6133: 'autofocus' is unused, ← would show as wavy underline TS6133: 'unrelatedImport' unused] │ ▼phaze-tsplugin wraps the response: ├── walks file's AST ├── finds <input use:autofocus={…}/> ├── records {autofocus} └── drops the autofocus diagnostic │ ▼ [TS6133: 'unrelatedImport' unused] ← only the genuinely-unused one survives │ ▼ wavy underline only on the genuinely-unused importOther unused-binding diagnostics survive untouched. Only the use:NAME-namespace false positives get filtered.
Why not just register the namespace handling in the TypeScript types?
Section titled “Why not just register the namespace handling in the TypeScript types?”A common reaction: “can’t @madenowhere/phaze ship a TypeScript type that says ‘a use:foo JSX attribute references the identifier foo’?”
Short answer: no — TypeScript’s reference graph isn’t a type-level concept. Types describe value shapes (what’s assignable to what); the reference graph is computed by the compiler’s binder pass walking AST nodes and recording identifier mentions. There’s no way to express “this JSX attribute counts as a reference to this identifier” in a .d.ts file. The reference tracker has to be modified by patching the language service at runtime, which is exactly what a Language Service plugin does.
This is the same mechanism Astro’s TS plugin uses to teach TypeScript about .astro files, the same mechanism Vue’s TS plugin uses for SFC blocks, and the same mechanism Prisma’s plugin uses for generated client types. All of them sit at the same “patch the diagnostic stream” layer.
Why it’s a separate package from babel-plugin.ts in phaze-compiler
Section titled “Why it’s a separate package from babel-plugin.ts in phaze-compiler”The babel plugin lives at packages/compile/src/babel-plugin.ts and is the engine inside phaze-compile. It’s the one that actually rewrites the use:autofocus namespace attribute into the post-creation IIFE that references autofocus. So why isn’t the editor-side plugin part of the same package?
Three reasons:
-
Different host APIs, different ASTs.
babel-plugin.tsin phaze-compiler uses Babel’sPluginObj<{ visitor: … }>API and Babel’s AST shape (@babel/types). phaze-tsplugin usestsTypes.LanguageServicewrap and TypeScript’s compiler API AST (ts.SourceFile,ts.JsxAttribute, …). The two AST shapes are structurally similar but the type definitions are not interchangeable, and the plugin contracts have no shared base. -
Different runtime processes.
babel-plugin.tsin phaze-compiler runs inside the build’s Node process — once per file, against your committed source, as part of Vite’s transform pipeline. phaze-tsplugin runs inside the TypeScript Language Server process the editor spawns (tsserver) — continuously, against your in-memory editor buffer, every keystroke. Shipping them as one package would force editor-only consumers (who never need Babel) to install Babel, and force build consumers (who never need TypeScript’s LanguageService API) to installtypescript/lib/tsserverlibrary. -
Different concerns.
babel-plugin.tsproduces transformed source — generates code. phaze-tsplugin filters diagnostics — modifies a read-only output. One writes; the other reads-and-suppresses. Combining them into one codebase would tangle two completely different mental models.
The two are complementary: babel-plugin.ts in phaze-compiler makes your use:foo syntax actually do something at runtime. phaze-tsplugin makes the editor stop yelling at the import that babel-plugin.ts in phaze-compiler will use to do that. You need both to get clean Phaze DX.
Why an editor-only tool is worth shipping
Section titled “Why an editor-only tool is worth shipping”It’s 200 lines and an opt-in tsconfig.json entry. Marginal install/maintenance cost. The payoff: every directive-importing file in your app stops flagging false unused-import errors, and noUnusedLocals: true CI passes work end-to-end without // eslint-disable / // @ts-expect-error boilerplate per file.
For a UI framework whose canonical surface relies on JSX namespace attributes (use:, class:, bind:), this is table stakes. Without it, the editor experience pushes users away from the namespaces — defeating the directive system before they ever ship a build.