Skip to content

7. phaze-vscode

1. phaze-tsplugin ← editor (TS Language Service)
2. phaze-compile ← build-time AST rewriting
3. phaze-vite ← island HMR + chunking helpers
4. 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) ← you are here
8. phaze-glow ← VSCode theme + halo runtime

madenowhere.phaze-vscode is the VSCode editor integration for .phaze files. It’s a marketplace extension — code --install-extension madenowhere.phaze-vscode — that contributes everything VSCode needs to treat .phaze files as a first-class language: an extension-to-language association, a TextMate grammar for syntax highlighting, a language configuration for brackets / comments / auto-close, and an LSP client that launches phaze-language-tools (#6) for type-aware features.

It’s deliberately the thin layer of the editor stack. The heavyweight work — parsing .phaze, synthesizing the virtual .tsx, running the TypeScript service against it, mapping responses back to .phaze positions — all lives in #6. phaze-vscode just bridges VSCode’s extension contract to that backend.

The extension contributes three things via package.json:

"languages": [
{
"id": "phaze",
"extensions": [".phaze"],
"aliases": ["Phaze", "phaze"],
"configuration": "./language-configuration.json"
}
]

Now VSCode knows that .phaze files belong to the phaze language, that “Phaze” is the human-readable label (status bar + Command Palette → Change Language Mode), and that its bracket / comment / auto-close rules live in language-configuration.json. Themes and other extensions can target the phaze language ID.

"grammars": [
{
"language": "phaze",
"scopeName": "source.phaze",
"path": "./syntaxes/phaze.tmLanguage.json",
"embeddedLanguages": { "source.tsx": "typescriptreact" }
}
]

The grammar (syntaxes/phaze.tmLanguage.json) has three rules:

  • Named fence (---page, ---data, ---signals, ---state) — distinct token scope so themes can color them as section markers.
  • Bare fence (---) — separate scope for the page-mode transition fence between the head and the body.
  • TSX body — everything that’s not a fence is included via source.tsx. VSCode’s embedded-language machinery lets the typescriptreact grammar run over those ranges, so JSX / TS / template literals / regex all colorize natively without re-implementing the TSX grammar.

embeddedLanguages: { "source.tsx": "typescriptreact" } is the magic — VSCode delegates bracket matching (⌘B), comment toggling (⌘/), and snippet expansion inside TSX ranges to the typescriptreact language. Without it those features would fall back to plain text.

language-configuration.json mirrors .tsx for brackets ((), [], {}), auto-close pairs ("", '', “, (), [], {}), comment toggles (//, /* */), surrounding pairs, and folding markers. The reader gets the same editing ergonomics they expect from a .tsx file.

On .phaze file open, src/extension.js launches phaze-language-tools as a stdio child process via vscode-languageclient:

const { LanguageClient, TransportKind } = require('vscode-languageclient/node')
async function activate(context) {
// Resolve the language server's bin entry from this extension's
// node_modules. require.resolve walks the actual resolver, so it
// handles pnpm symlinks, hoisting, and workspace links uniformly.
const serverModule = require.resolve(
'@madenowhere/phaze-language-tools/bin/phaze-language-server.js',
)
// Locate the workspace's TypeScript SDK — the user's
// `node_modules/typescript/lib` is the source of truth so the LSP
// matches whatever tsc version the project depends on.
const workspaceFolder = vscode.workspace.workspaceFolders?.[0]
const tsdk = path.join(workspaceFolder.uri.fsPath, 'node_modules', 'typescript', 'lib')
const serverOptions = {
run: { module: serverModule, transport: TransportKind.ipc },
debug: { module: serverModule, transport: TransportKind.ipc,
options: { execArgv: ['--nolazy', '--inspect=6009'] } },
}
const clientOptions = {
documentSelector: [{ scheme: 'file', language: 'phaze' }],
initializationOptions: { typescript: { tsdk } },
}
client = new LanguageClient('phazeLanguageServer', 'Phaze Language Server',
serverOptions, clientOptions)
await client.start()
context.subscriptions.push({ dispose: () => client?.stop() })
}

Three details worth understanding:

  • require.resolve is the bin-finder. @madenowhere/phaze-language-tools ships as a workspace-sibling dependency of the extension; require.resolve('@madenowhere/phaze-language-tools/bin/…') runs Node’s actual resolver, so pnpm’s isolated node_modules, npm’s hoisted layout, and link: symlinks all work without per-environment path twiddling. Same pattern Astro’s VSCode extension uses.
  • TransportKind.ipc runs the server in a Node child process talking JSON-RPC over the parent-child IPC channel. No port allocation, no stdio buffering surprises. The debug config is identical except for --nolazy --inspect=6009 so you can attach a debugger to the language server during development without touching the production runtime.
  • initializationOptions.typescript.tsdk is the absolute path to the workspace’s TS SDK — phaze-language-tools loads tsc from there instead of bundling its own, so type-checking matches whatever tsc version your project depends on. If the user has no workspace folder open, the extension surfaces a warning and returns (.phaze IntelliSense needs a workspace with TypeScript installed).

What VSCode users see once it’s running:

  • Hover on a signal() call inside a fence shows the real Signal<T> type with the inferred generic.
  • Completion on . after a signal binding lists (), .set, .update, .subscribe, etc.
  • Go-to-definition on a directive name (use:autofocus) jumps to its export in phaze-directives.
  • Find references crosses the .phaze ↔ virtual .tsx boundary transparently.
  • Diagnostics (TS errors) appear at the correct .phaze line.

The mapping back to .phaze positions happens inside Volar (see phaze-language-tools) — the extension is just a transport.

#7 and #8 are deliberately split. #7 contributes structural language support (grammar / LSP / language configuration) — works with any color theme. #8 (phaze-glow) is a color theme + an optional workbench patcher for the halo effect — works on any source language, not just .phaze. Splitting them lets a user adopt:

  • Just #7.phaze IntelliSense + their existing theme.
  • Just #8 — Phaze Glow colors / halo on any source language.
  • Both — the full Phaze visual identity.

If they were one extension, you’d have to choose between forcing the theme on every install (rude to users who like Solarized) or hiding the IntelliSense behind a theme switch (unhelpful). Two extensions, two clean install paths.

The two extensions also have different release cadences: language services updates ship whenever phaze-language-tools (#6) ships, which tracks phaze-compile changes. Theme tweaks (color palette adjustments, halo brightness defaults) ship on their own rhythm. Separate VSIX files = separate version histories on the marketplace.

  • No semantic highlighting beyond TS. Token coloring on .phaze fences comes from the TextMate grammar; coloring inside the TSX body comes from the embedded typescriptreact grammar + the TS service’s semantic tokens. There’s no Phaze-specific semantic colorization.
  • No formatter. Prettier handles .tsx and ignores .phaze. A v2 formatter would run phaze-compile’s parser, format each fence body via Prettier’s TSX printer, and reassemble — out of scope for now.
  • No .phaze file template. No “New Phaze File” command; users start from .tsx and rename. A snippet pack could ship later if there’s demand.
  • No build / dev-server orchestration. The extension doesn’t run pnpm dev; it’s pure editor surface. Build/dev concerns belong to phaze-vite and the host adapters (#4 / #5).
Terminal window
code --install-extension madenowhere.phaze-vscode

Or search “Phaze” by publisher madenowhere in the Extensions view.

The extension activates on first .phaze open — no per-project config. To pair with phaze-glow:

Terminal window
code --install-extension madenowhere.phaze-vscode
code --install-extension madenowhere.phaze-glow

Then switch theme via Command Palette → Color Theme → Phaze Glow.

PathRole
package.jsonExtension manifest — language ID, grammar, language-configuration, vscode-languageclient dep, @madenowhere/phaze-language-tools dep.
language-configuration.jsonBrackets, comments, auto-close pairs, indentation, folding — mirrors .tsx.
syntaxes/phaze.tmLanguage.jsonTextMate grammar. Three rules: named fence (---<label>), bare fence (---), TSX-body include (source.tsx).
src/extension.jsActivation entry. Resolves the LSP bin via require.resolve, locates the workspace TS SDK, launches phaze-language-server over IPC.