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:

pts47-program-stack.svg

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:

pts47-vite-duality.svg

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

  1. 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?
  2. Environment API: does a plugin's transform receive 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.
  3. Where does the dev PluginContainer reimplementation diverge from Rollup's real container? A named list of known behavior gaps is worth more than the happy-path walkthrough.
  4. Is plugin ordering deterministic for plugins sharing the same enforce value? 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

  1. Highest value. The Strada-to-Corsa question. TypeScript 7 drops the Strada API and the language service moves to LSP. Does the proxy-LanguageService pattern 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.)
  2. 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?
  3. The portability claim: has the same plugin actually run under Neovim or Sublime, or is "theoretically portable" still theoretical?
  4. 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:

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

    pts47-tsgo-split.svg

    The type-check / emit boundary stops being an implementation detail and becomes load-bearing pipeline architecture.

  3. Behavioral divergence. 74 of 20000 compiler test cases differ from tsc 6.0. Defaults shift: strict on by default, target to latest stable ECMAScript, es5 removed, baseUrl removed, node10 resolution removed. The JSDoc path is rewritten and no longer recognizes @enum or @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

  1. 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?
  2. 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.
  3. Determinism: is the worker file partition stable across TS patch versions? Can changing --checkers ever change diagnostics rather than only wall time? It should not. Confirm.
  4. Watch mode is documented as less efficient in some cases. By how much, and what is the recommended dev loop, nodemon plus tsgo --incremental?
  5. 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.

Author: Jason Walsh

jwalsh@nexus

Last Updated: 2026-05-19 09:27:33

build: 2026-05-19 23:11 | sha: 5cfabd4