Verified facts about the environment this plugin runs in. Updated 2026-09-28; each item names the version it was checked against. Re-verify before relying on an item older than a few months.
- The Node tsserver host loads plugins with a synchronous
require()(TypeScript 5.9 and 6.0:src/compiler/sys.tsnodeSystem.require,src/server/project.tsimportServicePluginSync). The asyncimportPluginpath exists only for hosts that implement it; the Node CLI host does not. - Resolution uses classic Node10 rules from the host's own directory or
--pluginProbeLocations:package.jsonmainis what tsserver finds, not theexportsmap (NodeResolutionFeatures.None, noExportsfeature bit, intryResolveJSModuleWorker). The plugin probe is relative to the realpath of thetypescriptpackage tsserver loaded, not the project directory, so a symlinkedtypescriptinstall (a version-manager shim, a monorepo hoist) sends the probe to the symlink target's tree; a test harness assembling an isolated project must copytypescriptin, not symlink it. - TypeScript 4.4 and older do not resolve a
.mjsmainat all ("Could not resolve JS module" in the tsserver log). The same is true of a.cjsmain: measured against a real tsserver on TypeScript 3.9, 4.0, and 4.4 withmain: "./lib/index.cjs", the log shows the same "Could not resolve JS module" failure, tryinglib/index.cjs.js,lib/index.cjs/index.js, and similar mangled suffixes rather than the literal file; TypeScript 4.9 and newer resolve.cjscorrectly. This package's rootmainislib/index.js(a real CommonJS file, disambiguated from the package root's"type": "module"by a nestedlib/package.json), which every supported TypeScript version's Node10 resolution finds. typesVersions(package.json) applies to this same classic-resolution code path, not only to a consumer's own type lookup:tryResolveJSModuleWorkershares the "resolvemainor a subpath" logic with the types resolver, so atypesVersionsentry keyed on the same relative path asmain(here,lib/indexorlib/index.js) redirects the plugin loader's own runtimerequire()to the declaration file instead of the implementation, which then throwsSyntaxError: Cannot use import statement outside a moduletrying to execute it. Measured on TypeScript 5.0 by adding and removing such an entry against a real tsserver. AtypesVersionsentry for any OTHER subpath (onemainitself never resolves to, such as./api,./lib/api, or./lib/api.jshere) does not collide, since the plugin loader never requests those paths.- The configuration a plugin receives at creation (
PluginCreateInfo.config) is the whole tsconfigpluginsentry,nameincluded. A plugin an editor loads globally (--globalPlugins, as VS Code does for an extension'stypescriptServerPlugins) receives{ name, global: true }, and aconfigurePluginpayload that arrives before the project loads replaces the entry, withnameset on it. A laterconfigurePluginpayload reachesonConfigurationChangedas sent. Checked in TypeScript 5.0.4 (lib/tsserver.js) and 6.0.3 (lib/typescript.js):enableGlobalPlugins,endEnablePlugin,enableProxy,onPluginConfigurationChanged. - A plugin that fails to load or activate never surfaces to the user: the only trace is the tsserver log. An empty completion response from tsserver also reports
success: false, so harnesses must read the log to distinguish "plugin skipped" from "no results". - Helix plugin setup (Helix 25.07.1 and
typescript-language-server6.0.1, checked 2026-09-28): Helix mergeslanguages.tomlthrough three table levels, so a user-providedlanguage-server.typescript-language-server.configreplaces that server's built-inconfigtable. A plugin example must repeat the built-inhostInfoand TypeScript/JavaScript inlay-hint settings to preserve them.typescript-language-serverpasses each initialization plugin'slocationdirectly to tsserver through--pluginProbeLocations; its acceptedlocationforms resolve under the classic Node10 rules above.
typescript@lateston npm is 7.0.2, the native compiler. The package ships notsserverand has no plugin API; VS Code's TypeScript 7 mode does not loadtypescriptServerPlugins. Plugins run only on TypeScript 5.x and 6.x hosts; an older host can still load the plugin module itself without erroring, since this plugin's own version gate (below) returns the host's language service untouched rather than activating.- VS Code stable bundles TypeScript 6 (
npm:@typescript/typescript6). - Floor for this plugin: TypeScript 5.0 (
major >= 5,isSupportedTypeScriptVersion,src/tsserver/tsserver-plugin.ts). 1.0.1 gates onmajor >= 3, buttypescript-template-language-service-decorator2.3.2 (the last release) bindslanguageService.getSupportedCodeFixeswhenever the template service passed to it implements the same method, regardless of host version; that instance method exists onts.LanguageServiceonly from TypeScript 5.0 (confirmed viats.createLanguageService(...).getSupportedCodeFixesreturningundefinedon 3.9, 4.0, 4.4, and 4.9, and a function from 5.0), so binding it on an older host throwsTypeError: Cannot read properties of undefined (reading 'bind')before any feature activates. Reproduced against a real tsserver on TypeScript 3.9, 4.0, 4.4, and 4.9 with the published decorator's own logged message: "Plugin activation failed: TypeError: Cannot read properties of undefined (reading 'bind')". 1.0.1 crashes identically on these versions today, since a fresh install resolves the same final decorator release. This plugin's own gate never constructs the decorator below TypeScript 5.0, logging "Unsupported TypeScript version ... TypeScript 5.0 or newer required" and returning the host's own language service untouched instead of crashing there. - Line terminators:
ts.computeLineStartsstarts a new line after\r\n, a lone\n, a lone\r, U+2028, and U+2029, and not after U+0085. Checked on TypeScript 4.9.5, 5.0.4, 5.9.3, and 6.0.3 (computeLineStarts("a\r\nb\nc\rd\u2028e\u2029f\u0085g")returns[0,3,5,7,9,11]on each). - A declaration file using a string export name (
export { x as 'module.exports' }) fails to parse (TS1003) on TypeScript 5.0 through 5.5; 5.6 accepts it. This package no longer emits that syntax anywhere (the root entry's declaration usesexport =, parseable since TypeScript's earliest CommonJS-module support); kept here as the reason a future root-entry declaration should avoid the string-export-name form.
- The package root (the tsserver plugin factory) is plain CommonJS:
require()works on every Node version this plugin's dependencies support, with norequire(esm)interop involved. tsdown'stargetfor every output (root CJS,./apiCJS,./apiESM) isnode14, so syntax this package's own source uses (||=, optional chaining, and similar) is transpiled to a Node 14-safe form; tsdown does not polyfill runtime APIs, and this package's source uses none introduced after Node 14 (checked: noArray.prototype.at,Object.hasOwn,structuredClone, orString.prototype.replaceAll). Measured with the packed tarball's root, bothrequire()andimport(): Node 14.21.3, 16.20.2, 18.20.5, 20.18.1, 22.12.0, and 24.11.0 all load it and return the factory (import()gets it as the default export, since Node's ESM loader synthesizes a default from a CommonJS module'smodule.exports). - Node 12.22.12 fails for both this package and 1.0.1, from the same cause neither controls: this package's runtime
dependencies(vscode-css-languageservice6.3.10,@vscode/emmet-helper2.11.0) ship optional chaining (?.) in their own published CommonJS builds (vscode-css-languageservice/lib/umd/cssLanguageService.js,@vscode/emmet-helper/lib/cjs/emmetHelper.js), which Node 12 does not parse (SyntaxError: Unexpected token '.') regardless of this package's own build target. 1.0.1's^6.2.11/^2.9.2dependency ranges resolve to these same exact versions today (a freshnpm installof 1.0.1 getsvscode-css-languageservice@6.3.10and@vscode/emmet-helper@2.11.0, identical to this package's pinned versions), so 1.0.1 fails identically: reproduced against a real tsserver on TypeScript 3.9, 4.4, and 4.9, both packages showing no plugin activation (log: "Failed to load module '@styled/typescript-styled-plugin'...vscode-css-languageservice/lib/umd/cssLanguageService.js...SyntaxError: Unexpected token '.'" for 1.0.1; this package's own bundle fails one line into its own code for the same underlying reason, since its build target permits, rather than removes, optional chaining). Node 14.21.3 is therefore the actual common floor, not Node 16.20.2: 1.0.1's own actual floor, even though 1.0.1 never declared one. ./apiis a real dual build:importgetslib/esm/api.mjs,requiregetslib/esm/api.cjs, a genuine CommonJS bundle. Measured with the packed tarball's./api, bothrequire()andimport()succeed on Node 14.21.3, 16.20.2, 18.20.5, 20.18.1, 22.12.0, and 24.11.0 (the same Node floor as the package root, with norequire(esm)dependency).require()of an ES module throwsERR_REQUIRE_ASYNC_MODULEif the module graph uses top-levelawait. Keep thelib/esm/api.mjsbundle free of it.export { value as 'module.exports' }sets whatrequire()of an ES module returns; named exports are then invisible to CommonJS callers. Not used by this package (see the TypeScript-versions note above); documented here becauserequire(esm)interop depends on it.- Host runtimes: VS Code and Cursor run tsserver on their Electron Node (24.x in 2026).
typescript-language-server6.0.1 requires Node 22.22.2 or newer.@vtsls/language-serverdeclares Node 18 or newer, so a vtsls user on an older system Node cannot load the plugin.typescript.tsserver.nodePathin VS Code substitutes the user's Node.
typescript-template-language-service-decorator2.3.2 (2023) is the last release; the upstream repository is archived, so any fix needs a fork or vendoring. It is CommonJS.vscode-css-languageservice,@vscode/emmet-helper: CommonJSmainwith an ESMmodulefield and noexportsmap; Node always loads the CommonJS build.vscode-css-languageservice6.3.10's runtime enums (CompletionItemKind,DiagnosticSeverity, and similar) are not visible to a Node ESM named import: with noexportsmap, Node's ESM loader statically analyzes the CommonJS build to synthesize named exports, and that analysis does not find them. Measured on Node 24.11.0:node --input-type=module -e "import { CompletionItemKind } from 'vscode-css-languageservice'"throwsSyntaxError: Named export 'CompletionItemKind' not found, while the same import fromvscode-languageserver-types3.18.4 succeeds. This plugin imports those enums fromvscode-languageserver-typesinstead (src/features/completions.ts,src/features/diagnostics.ts,src/features/code-actions.ts), the packagevscode-css-languageserviceitself depends on for the same numeric values.
The decorator matches a tagged template by the tag's source text. A configured name n matches when the tag text is n, starts with n., n(, n<, or n[, or ends with .n. Only tagged templates are candidates: a plain call like globalStyle('body', {...}) never matches. Short generic names therefore also match unrelated tagged templates that share the prefix.
styled-components/vscode-styled-componentsbundles this package and registers it throughcontributes.typescriptServerPluginswithenableForWorkspaceTypeScriptVersions: true. The extension depends on^1.0.0; a new major reaches extension users only after the extension updates that range.
- Open the probe file without
projectFileNameand keep itstsconfig.jsonin the same directory: passingprojectFileNamemakes tsserver build an inferred project that ignores the config. - Derive caret positions from markers in the source, never hand-counted columns, and pair every assertion that expects nothing (no diagnostics, no completions) with a positive control in the same session, since a plugin that failed to load also returns nothing.
- Escape U+2028 and U+2029 in request JSON; the line-delimited protocol treats them as line breaks.
- Match responses by
request_seq, not by command name. - A notification (
open,change) gets a response on TypeScript 5.9.3 and 6.0.3 and none on 5.5.4, 5.0.4, and 4.9.5 (Session.notRequired), so a harness awaits only requests and discards a notification's response when one arrives. - Sending
openwithfileContentfor a file that is already open replaces its text and marks its projects dirty (ScriptInfo.open, 4.9.5 through 6.0.3), so one tsserver can serve many cases that each reopen the file with their own content. - tsserver tries
--pluginProbeLocationsbefore its own install'snode_modules(the log line "Enabling plugin ... from candidate paths"), so the e2e suite loads the plugin through thetest/e2eworkspace link; the e2e globalSetup (test/e2e/tsserver-fixture/check-plugin-link.ts) fails the run when that link resolves outside this repository. - To find tests that pass without the plugin, point that link at a stub package whose factory's
createreturnsinfo.languageServiceunchanged (and turn the link check off for the run): only tests that never depend on the plugin (TypeScript's own completions inside a placeholder, a missing-plugin project, the marker helper's own tests) may still pass.