How TSSLint Handles Virtual TypeScript Files for Vue, MDX, and Astro

TSSLint treats Vue, MDX, and Astro files as virtual TypeScript files by loading framework-specific language plugins from @volar/language-hub, decorating the TypeScript service host to expose virtual file names, and mapping diagnostics back to the original source files.

TSSLint provides first-class linting support for modern meta-frameworks by converting non-TypeScript source files into virtual TypeScript representations. This architecture enables type-aware lint rules to run against Vue Single File Components (SFCs), MDX documents, and Astro templates as if they were standard .ts files.

Language Plugin Discovery and Loading

The CLI entry point orchestrates framework support through the load() function in packages/cli/lib/languagePlugins.ts. This module dynamically instantiates language plugins based on command-line flags.

CLI Flags for Meta-Frameworks

TSSLint exposes dedicated flags for each supported framework:

  • --vue-project – Enables Vue SFC support via createVuePlugin
  • --vue-vine-project – Enables Vue Vine support via createVueVinePlugins
  • --mdx-project – Enables MDX support via createMdxPlugin
  • --astro-project – Enables Astro support via createAstroPlugin

Volar Language Hub Integration

Each flag triggers the corresponding factory function from @volar/language-hub. For example, when --vue-project is specified, the CLI executes:

createVuePlugin(ts, tsconfig)

These plugins implement the Volar language plugin API, which defines how to parse framework-specific syntax and generate virtual TypeScript files containing the extracted script content plus type declarations for template bindings.

Decorating the TypeScript Service Host

Once plugins are loaded, TSSLint integrates them into the TypeScript language service through host decoration in packages/cli/lib/worker.ts.

Virtual File Name Exposure

At line 189 in worker.ts, the CLI calls:

decorateLanguageServiceHost(ts, language, linterHost)

This function from @volar/typescript augments the LanguageServiceHost with three critical capabilities:

  1. Virtual file enumeration – Exposes generated .ts file names for each .vue, .mdx, or .astro source
  2. Snapshot provision – Implements getScriptSnapshot to return ts.IScriptSnapshot instances containing the virtual TypeScript content
  3. Extension registration – Adds framework extensions to typescript.extraFileExtensions

Enabling Non-TypeScript Extensions

To prevent the TypeScript compiler from rejecting non-standard extensions, worker.ts lines 103-108 check if any plugin contributes to extraFileExtensions. If so, the CLI sets:

allowNonTsExtensions: true

This configuration allows the language service to process .vue, .mdx, and .astro files without throwing extension-related errors.

Linting Virtual Files with the Core Engine

The core linting engine in packages/core/index.ts operates transparently on both physical and virtual files. After host decoration, the linter is instantiated at lines 58-74:

linter = core.createLinter(
    { languageService: linterLanguageService,
      languageServiceHost: linterHost,
      typescript: ts },
    path.dirname(configFile),
    config,
    () => [],
    linterSyntaxOnlyLanguageService,
);

The languageService now recognizes virtual TypeScript files as standard source files. When linter.lint(fileName, cache) executes, it processes the virtual .ts content generated from Vue templates, MDX components, or Astro frontmatter, enabling type-aware rules to validate framework-specific syntax.

Mapping Diagnostics to Source Files

When diagnostics originate from virtual files, TSSLint ensures users see errors mapped to their original source locations. In packages/core/index.ts lines 190-227, the createRelatedInformation function rewrites the file property of diagnostics:

// Simplified representation of the mapping logic
if (diagnostic.file && isVirtualFile(diagnostic.file.fileName)) {
    const sourceFile = getSourceFile(diagnostic.file.fileName);
    diagnostic.file = sourceFile;
}

This transformation ensures that error reports point to .vue, .mdx, or .astro files rather than internal virtual .ts shadows, providing a seamless developer experience.

Configuration and Usage Example

To lint Vue SFCs, MDX documents, or Astro templates, specify the framework flag when running the CLI:


# Lint Vue files

tsslint --vue-project ./tsconfig.json

# Lint MDX files

tsslint --mdx-project ./tsconfig.json

# Lint Astro files

tsslint --astro-project ./tsconfig.json

Configure rule inclusion in tsslint.config.ts:

// tsslint.config.ts
export default {
  include: ['**/*.vue', '**/*.mdx', '**/*.astro', '**/*.ts'],
  rules: {
    'no-console': 'error',
    'no-unused-vars': 'error',
  },
};

Summary

  • TSSLint handles Vue, MDX, and Astro files as virtual TypeScript files through the Volar language plugin system.
  • Framework plugins are loaded dynamically via packages/cli/lib/languagePlugins.ts based on CLI flags like --vue-project and --mdx-project.
  • The TypeScript service host is decorated in packages/cli/lib/worker.ts to expose virtual file names and enable non-TypeScript extensions.
  • The core linter operates transparently on virtual files in packages/core/index.ts, treating generated TypeScript from framework files as standard source.
  • Diagnostics are mapped back to original files so developers see errors in their .vue, .mdx, or .astro sources rather than virtual shadows.

Frequently Asked Questions

How does TSSLint differ from ESLint when linting Vue files?

TSSLint uses the TypeScript language service to create virtual TypeScript representations of Vue SFCs, enabling type-aware linting against actual TypeScript types extracted from templates. ESLint typically uses parsers like vue-eslint-parser to generate an AST, but does not inherently provide TypeScript type information for template bindings without additional configuration.

Can I use TSSLint with multiple meta-frameworks in the same project?

Yes. You can specify multiple framework flags simultaneously when running the CLI, such as tsslint --vue-project --mdx-project ./tsconfig.json. The load() function in packages/cli/lib/languagePlugins.ts aggregates all requested plugins into the languages array, and the decorated language service host handles virtual files from all frameworks concurrently.

What happens if a virtual file contains TypeScript errors in the template section?

When a Vue template contains type errors, the createVuePlugin generates a virtual TypeScript file that includes type declarations for template bindings. If these declarations conflict with actual types, the TypeScript language service reports diagnostics. The core linter in packages/core/index.ts then maps these diagnostics back to the original .vue file using the createRelatedInformation logic, displaying the error at the correct line in the template section.

Does TSSLint support custom language plugins beyond Vue, MDX, and Astro?

Yes. While TSSLint provides built-in CLI flags for Vue, Vue-Vine, MDX, and Astro, the architecture in packages/cli/lib/languagePlugins.ts is designed to accept any plugin conforming to the Volar language plugin API. You can theoretically pass custom language plugin factories to the load() function, provided they implement the required methods for generating virtual TypeScript files and providing script snapshots.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →