Paris TypeScript #47: Prep Notes
Vite plugins, TS server plugins, and tsgo. Going in with questions.
Table of Contents
Context
Paris TypeScript #47, hosted by Takima. Three 20-minute talks. This entry is preparation research, not a recap. The objective is to enter each talk with a refutation condition and two or three questions that the abstract cannot already answer.
The program is more coherent than three independent submissions. Read as a stack:
Talk 3 is the load-bearing one: it changes the ground that Talks 1 and 2 stand on. Talk 2 in particular teaches a technique built on an API that Talk 3 is about deprecating. That tension is the single best Q&A thread of the night.
Talk 1: Vite plugin pipeline (Estéban Soubiran, 20 min)
Title: Au coeur d'une pipeline: démystifions Vite et ses plugins.
Predicted arc
Vite history as motivation (esbuild-prebundled deps plus a native-ESM dev server, now the Rolldown era), then the plugin model, then a live walk through the orchestration using real plugins.
The conceptual payload is the dev/build duality. A Vite plugin is one object that runs in two execution contexts with different semantics:
Vite reimplements Rollup's PluginContainer so the same plugin instance
drives the dev server with no bundling step. The plugin author writes one
contract; it must hold under both contexts. That is the part worth
demystifying, and the part most plugin bugs come from.
Hook surface he will likely cover: Rollup build hooks (resolveId, load,
transform) plus Vite-specific hooks (config, configResolved,
configureServer, transformIndexHtml, hotUpdate). Plus ordering
(enforce: 'pre' | 'post') and context gating (apply: 'serve' | 'build').
The standard virtual-module idiom, almost certain to appear:
import type { Plugin } from 'vite' // The \0 prefix marks an id as virtual: other plugins and the // resolver leave it alone, and it does not hit the filesystem. const VIRTUAL = 'virtual:build-info' const RESOLVED = '\0' + VIRTUAL export function buildInfo(): Plugin { return { name: 'wal-sh:build-info', enforce: 'pre', // ordering relative to other plugins resolveId(id) { if (id === VIRTUAL) return RESOLVED }, load(id) { if (id === RESOLVED) { return `export const builtAt = ${Date.now()}` } }, // Vite-only hook: has no Rollup equivalent, dev-server scoped. configureServer(server) { server.middlewares.use('/__buildinfo', (_req, res) => { res.end('ok') }) }, } }
"Past to future" almost certainly lands on Rolldown (Rust port of Rollup) as the unified bundler and the Environment API (one module graph across client, SSR, and edge runtimes such as workerd). "Only Next resists" because Next owns its compiler (Turbopack) and will not cede the extension surface.
Questions to bring
- Highest value. Rolldown migration: which Rollup hooks change semantics,
and is the Vite plugin interface versioned against that change? The long
tail of
vite-plugin-*is the real risk surface. What is the compat contract during Rollup-to-Rolldown? - Environment API: does a plugin's
transformreceive which environment it runs for, as a first-class argument? Writing a plugin that behaves differently client vs SSR vs edge without branching on ambient globals is the open question. - Where does the dev
PluginContainerreimplementation diverge from Rollup's real container? A named list of known behavior gaps is worth more than the happy-path walkthrough. - Is plugin ordering deterministic for plugins sharing the same
enforcevalue? If not, how do you debug an order-dependent failure?
Refutation condition
Prediction misses if the talk stays at "here is the hook list" and never addresses the dev/build dual execution or the Rolldown transition. If it is purely a hook catalogue, the questions above still hold and become more urgent, not less.
Talk 2: TS server plugin for a VS Code extension (Luther Tchofo Safo, 20 min)
Title: Comment créer un plugin serveur TypeScript pour une extension VS Code. Framed as a war story (retour d'expérience).
Predicted arc
The abstract names three pain points: absent docs, missing or incomplete typings, buried logs. Expect the talk to be structured around defeating each.
A TS server plugin is a module that returns a proxied LanguageService.
Decorator pattern over ts.LanguageService: override the methods you care
about, delegate the rest.
import type tsModule from 'typescript/lib/tsserverlibrary' function init({ typescript: ts }: { typescript: typeof tsModule }) { function create(info: tsModule.server.PluginCreateInfo) { // info.languageService is the real service. Proxy it. const proxy: tsModule.LanguageService = Object.create(null) for (const k of Object.keys(info.languageService) as Array< keyof tsModule.LanguageService >) { const orig = info.languageService[k]! // @ts-expect-error - the index signature is under-typed; this is // exactly the missing-typings pain the abstract refers to. proxy[k] = (...args: unknown[]) => orig.apply(info.languageService, args) } // Logs go to the tsserver log file, not the extension host console. // Enable: typescript.tsserver.log = "verbose"; // Open: command "TypeScript: Open TS Server log". info.project.projectService.logger.info('wal-sh plugin: create()') proxy.getCompletionsAtPosition = (fileName, position, options) => { const prior = info.languageService.getCompletionsAtPosition( fileName, position, options, ) // augment prior.entries here for an embedded DSL, etc. return prior } return proxy } return { create } } export = init
The isolation gotcha is the spine of the story: the plugin runs inside the
tsserver process, not the extension host. Separate Node context, no DOM, no
shared state with the extension. That is why the logs are buried and why
debugging means TSS_DEBUG or attaching --inspect to tsserver.
Distribution is the only VS-Code-specific part: the
contributes.typescriptServerPlugins contribution point plus
enableForWorkspaceTypeScriptVersions. Version skew between the bundled TS
and the workspace TS is the other classic failure mode.
"Portable to any TypeScript environment" is correct for the technique: tsserver is editor-agnostic (Neovim via coc, Sublime, others drive the same server). Only the registration is VS-Code-shaped.
Questions to bring
- Highest value. The Strada-to-Corsa question. TypeScript 7 drops the
Strada API and the language service moves to LSP. Does the
proxy-
LanguageServicepattern survive TS7 at all? Is there a Corsa plugin API spec to target, or is this technique on a deprecation clock? (Pair this with Talk 3.) - Latency budget: the plugin runs in-process with tsserver and on the completion hot path. What is the practical ceiling before completions feel slow, and how do you keep augmentation off the critical path?
- The portability claim: has the same plugin actually run under Neovim or Sublime, or is "theoretically portable" still theoretical?
- Structured diagnostics: any way to get machine-readable logs out of tsserver, or is grep on the log file the state of the art?
Refutation condition
Prediction misses if the talk is forward-looking and TS7-aware rather than a war story about the current API. If Tchofo Safo opens with the Corsa transition and treats the proxy pattern as legacy, the abstract undersold the talk, and question 1 is already answered. More likely the abstract is accurate and the TS7 angle is the missing piece you supply in Q&A.
Talk 3: tsgo, 10x faster at what cost (Lilian Saget-Lethias, 20 min)
Title: tsgo : 10x plus rapide, mais à quel prix? The abstract pre-commits to going past benchmarks to what changes for teams shipping to production. High-confidence prediction: the facts are public.
The price, concretely
Three real costs, in order of blast radius:
- Strada API dropped. Anything depending on the old compiler API breaks until ported to the in-progress Corsa API: typescript-eslint, ts-morph, ts-patch / tspc, API Extractor, and TS server plugins (see Talk 2).
Emit pipeline incomplete. Native downlevel currently reaches only es2021, no decorator emit. tsgo is a type-checker today, not your emitter. The realistic deployment is a split:
The type-check / emit boundary stops being an implementation detail and becomes load-bearing pipeline architecture.
- Behavioral divergence.
74of20000compiler test cases differ from tsc 6.0. Defaults shift: strict on by default, target to latest stable ECMAScript, es5 removed,baseUrlremoved, node10 resolution removed. The JSDoc path is rewritten and no longer recognizes@enumor@constructor, which can surface new errors in JS-heavy projects.
The migration contract: TypeScript 6.0 is the mandatory stepping stone, the last JavaScript-based release. The transition pattern is running tsc and tsgo in parallel in CI and alerting on divergence.
The engineering-substance section, if the talk is good, is determinism under
parallelism: a fixed pool of type-checker workers, each owning a
deterministic file partition, so diagnostics are reproducible regardless of
worker count. Flags: --checkers, --builders, --singleThreaded.
Pre-meetup probe
Run this on a real repo before the meetup so the benchmark discussion is grounded in your own numbers, not Microsoft's.
# tsgo-probe.sh - compare tsc vs tsgo on the current project. # Run from a repo root with a tsconfig.json. set -euo pipefail npm install -D @typescript/native-preview >/dev/null 2>&1 || true echo "== tsc (Strada) ==" time npx tsc --noEmit 2> tsc.err || true wc -l < tsc.err | xargs echo "tsc diagnostics (lines):" echo echo "== tsgo (Corsa) ==" time npx tsgo --noEmit 2> tsgo.err || true wc -l < tsgo.err | xargs echo "tsgo diagnostics (lines):" echo echo "== divergence ==" # Non-empty diff here is your real migration backlog. diff <(sort tsc.err) <(sort tsgo.err) || echo "diagnostics differ (see above)" echo echo "== determinism check ==" # Changing --checkers must not change diagnostics, only wall time. npx tsgo --noEmit --checkers 2 2> c2.err || true npx tsgo --noEmit --checkers 8 2> c8.err || true diff <(sort c2.err) <(sort c8.err) \ && echo "OK: diagnostics stable across --checkers" \ || echo "WARN: --checkers count changed diagnostics, file a bug"
A non-empty divergence is your migration backlog. A non-empty determinism diff is a question for the speaker (and a bug report).
Questions to bring
- Highest value. Strada API death: realistic timeline for the Corsa API to be stable enough that typescript-eslint and ts-morph work natively. Until then, every team runs two compilers. Is that the official recommendation through 2026?
- Is the type-check / emit split transitional, or permanent architecture? If permanent, "the TypeScript compiler" stops being one thing, and team mental models need to change.
- Determinism: is the worker file partition stable across TS patch
versions? Can changing
--checkersever change diagnostics rather than only wall time? It should not. Confirm. - Watch mode is documented as less efficient in some cases. By how much, and
what is the recommended dev loop, nodemon plus
tsgo --incremental? - Is the 5.x to 6.0 to 7.0 path actually mandatory, or can a disciplined team skip 6.0?
Refutation condition
Prediction misses if the talk is benchmark theatre with no treatment of the Strada API loss or the emit gap. The abstract's "à quel prix" framing makes that unlikely; if it happens, the abstract oversold and questions 1 and 2 become the whole conversation.
The cross-talk thread for Q&A
Talk 2 teaches a TS server plugin technique built entirely on the Strada API. Talk 3's subject is the migration that deprecates that API and moves the language service to LSP. The meetup has, probably without planning it, programmed a technique and its obsolescence back to back.
If Tchofo Safo does not address the Corsa / LSP transition, that is the first audience question. Saget-Lethias, as an organizer, will know the answer. The good version of this question is not "is my plugin doomed" but:
Is there a stated migration path for TS server plugins to the Corsa API, or is the LSP move expected to absorb that extension surface entirely so the plugin model goes away rather than ports?
That distinguishes "port your plugin" from "the extension point itself is being removed," and the two have very different consequences for anyone maintaining one.
Open questions carried out of this prep
- Vite: is the plugin contract going to be explicitly versioned across the Rolldown cut, or is it best-effort compat?
- TS server plugins: does a Corsa plugin API exist on paper yet?
- tsgo: type-check / emit split, transitional or permanent?
These three are the test of whether the night was worth attending. If all three get concrete answers, the program delivered.
