9. phaze-check
1. phaze-tsplugin ← editor (TS Language Service)2. phaze-compile ← build-time AST rewriting3. phaze-vite ← island HMR + chunking helpers4. phaze-astro ← Astro integration (island model)5. phaze-cloudflare ← native Cloudflare Workers adapter (whole-page)
─── Editor stack ────────────────────────────────────────────────────6. phaze-language-tools ← Volar LSP backend (.phaze → virtual .tsx)7. phaze-vscode ← VSCode extension (grammar + LSP client)8. phaze-glow ← VSCode theme + halo runtime
─── Check ───────────────────────────────────────────────────────────9. phaze-check ← headless tsc wrapper for CI ← you are here@madenowhere/phaze-check is the CI-side half of the editor stack. Where phaze-vscode drives phaze-language-tools as an LSP for interactive hover types and diagnostics, phaze-check drives the same LanguagePlugin against the real tsc binary for headless type-checking — exit non-zero on any error, suitable for a GitHub Actions step or a pre-commit hook. Same pattern as vue-tsc, svelte-check, and astro check — the canonical “framework-aware tsc” pattern across the ecosystem.
Why tsc alone isn’t enough
Section titled “Why tsc alone isn’t enough”tsc doesn’t know .phaze files exist. The extension isn’t in supportedTSExtensions, so:
tsc --noEmitsilently skips every.phazefile — CI never catches type errors in pages written as.phaze.- Adding
.phazetotsconfig.json’sincludearray produces aFile '…' has an unsupported file extensionerror. - Adding
allowJs: truedoesn’t help —.phazeisn’t.jseither.
phaze-check is the bridge: it teaches tsc about .phaze and runs the same checker over the synthesised .tsx so type errors are caught exactly as they would be in handwritten .tsx.
Drop-in replacement for tsc --noEmit in CI:
pnpm add -D @madenowhere/phaze-check{ "scripts": { "check": "phaze-check --noEmit" }}All tsc flags pass through — the bin runs the real TypeScript compiler in-process, just with .phaze registered:
pnpm exec phaze-check --noEmitpnpm exec phaze-check --noEmit --watchpnpm exec phaze-check --project tsconfig.app.jsonpnpm exec phaze-check --noEmit --noUnusedLocalsExit codes follow tsc:
0— no errors.1— fatal (config error, missing file).2— type errors found.
Diagnostics print at .phaze line / column positions, e.g.:
src/pages/about.phaze(12,14): error TS2322: Type 'string' is not assignable to type 'number'.The mapping back from the synthetic .tsx happens inside Volar via the v3 sourcemap phaze-compile emits — same path the LSP uses, so error positions match what you’d see in VSCode.
What it does not do
Section titled “What it does not do”- No autofix. Same scope as
tsc— report errors, exit non-zero. Fixing the code is on you. - No formatter. Use Prettier (or a future Phaze formatter — out of scope).
- No
phaze-compilediagnostics. Fence-shape / format-parse errors surface at build time via the Vite plugin’s code-frame overlay (Phase 2b).phaze-checkis type-error-only; for full coverage, runpnpm build(catches format errors) andpnpm check(catches type errors) in CI.
How it works
Section titled “How it works”The bin is ~30 lines:
import { createRequire } from 'node:module'import { getPhazeLanguagePluginForTsc } from '@madenowhere/phaze-language-tools'
const require = createRequire(import.meta.url)
export function runPhazeCheck() { const tscPath = require.resolve('typescript/lib/tsc.js') const { runTsc } = require('@volar/typescript/lib/quickstart/runTsc.js')
return runTsc( tscPath, ['.phaze'], () => [getPhazeLanguagePluginForTsc()], )}#!/usr/bin/env nodeimport { runPhazeCheck } from '../dist/run.js'runPhazeCheck()Three pieces matter:
require.resolve('typescript/lib/tsc.js')finds the project’stscbinary on disk — Node’s resolver walks workspace symlinks, hoisting, and pnpm’s isolatednode_modulesuniformly, sophaze-checkuses the project’s TypeScript version, not its own bundled copy. Matches whatever tsc version your build does.@volar/typescript’srunTscis the canonical helper Volar ships for this pattern. It hot-patchestsc.jsat load time: interceptsfs.readFileSyncfor the tsc path, rewrites the source to inject the extra extension + aproxyCreateProgramcall that consults ourLanguagePlugin, thenrequire()s the patched source in-process. The original tsc binary on disk is never modified — the rewrite happens in memory.getPhazeLanguagePluginForTsc()is the string-keyed flavor of the sameLanguagePluginphaze-language-tools exposes for the LSP (URI-keyed) — see Two factories. Same parser, same emit, same v3 sourcemap.
Visually:
phaze-check (bin/phaze-check.js) │ ▼ src/run.ts │ ┌──────────────┴──────────────┐ ▼ ▼@volar/typescript @madenowhere/phaze- .runTsc(tscPath, language-tools ['.phaze'], .getPhazeLanguage () => [plugin]) PluginForTsc() │ │ └──────────────┬──────────────┘ ▼ patched real `tsc.js` (TypeScript compiler — your project's version) │ ▼ exit 0 (clean) / 2 (type errors)Why a separate package from #6
Section titled “Why a separate package from #6”phaze-language-tools is the engine; phaze-check is one consumer of it. They split because:
- Different runtime hosts.
#6runs inside an LSP server process spawned by the editor — long-lived, continuous, against in-memory buffers.phaze-checkruns inside a CI Node process — one-shot, against on-disk files, exits when done. - Different deps.
#6pulls in@volar/language-server,@volar/language-service,vscode-uri, the stdio transport.phaze-checkonly needs@volar/typescript(forrunTsc) plus#6’sLanguagePluginfactory. CI projects that don’t want VSCode-extension deps installed alongside their checker get a clean install. - Different consumers.
#6is consumed by#7(VSCode extension), future Cursor / JetBrains / Sublime integrations, andphaze-check. Bundlingphaze-checkinto#6would force every editor adapter to install the CLI bin path; bundling#6’s LSP machinery intophaze-checkwould force every CI project to install the language-server bits they never use.
The two share a LanguagePlugin factory — getPhazeLanguagePluginForTsc() for the headless string-keyed case, getPhazeLanguagePlugin() for the LSP’s URI-keyed case. Same body, same parser, same sourcemap; the key shape is the only difference, and both wrap an internal makePhazeLanguagePlugin<K> to share the implementation.
| Path | Role |
|---|---|
package.json | Bin (phaze-check), deps on @madenowhere/phaze-language-tools + @volar/typescript, peer-dep on typescript. |
bin/phaze-check.js | ESM bin entry. Imports runPhazeCheck from dist/run.js, invokes it. |
src/run.ts | Wraps @volar/typescript’s runTsc with getPhazeLanguagePluginForTsc(). |
src/index.ts | Re-exports runPhazeCheck for programmatic embed (test harnesses, future phaze parent CLI). |
README.md | Install + CI recipes. |
Related
Section titled “Related”- phaze-language-tools — the Volar
LanguagePluginthis CLI consumes (and which the VSCode extension also drives, as an LSP). - phaze-vscode — interactive sibling. Same plugin, different transport.
- phaze-compile — owns the
.phazeparse / emit / sourcemap pipeline;phaze-check’s diagnostics route through the same v3 sourcemap.