import fs from 'node:fs';
import { parseChunks, resolveImport } from '@sterashima78/ts-md-core';
import type { LanguagePlugin } from '@volar/language-core';
} from '@volar/language-service';
import ts from 'typescript';
import { URI } from 'vscode-uri';
import { type TsMdVirtualFile, createTsMdPlugin } from './index.js';
export interface TsMdDiagnostic {
start: { line: number; character: number };
end: { line: number; character: number };
export interface TsMdDiagnosticsResult {
[file: string]: TsMdDiagnostic[];
export function createTsMdLanguageService(files: string[]) {
const scripts = new Map<URI, SourceScript<URI>>();
const plugin = createTsMdPlugin as unknown as LanguagePlugin<
let language!: Language<URI>;
language = createLanguage<URI>([plugin], scripts, (id) => {
if (scripts.has(id)) return;
if (typeof id === 'string') {
const m = /^(.*)__/.exec(id);
filePath = URI.parse(m[1]).fsPath;
const snapshot = ts.ScriptSnapshot.fromString(
fs.readFileSync(filePath, 'utf8') as unknown as string,
typeof id === 'string' ? URI.parse(id) : id,
for (const file of files) {
const uri = URI.file(file);
const snapshot = ts.ScriptSnapshot.fromString(
fs.readFileSync(file, 'utf8') as unknown as string,
language.scripts.set(uri, snapshot, 'ts-md');
const ls = createLanguageService(language, [], { workspaceFolders: [] }, {});
export async function collectDiagnostics(
): Promise<TsMdDiagnosticsResult> {
const { language, ls } = createTsMdLanguageService(files);
const result: TsMdDiagnosticsResult = {};
for (const file of files) {
const uri = URI.file(file);
language.scripts.get(uri);
let diags = await ls.getDiagnostics(uri);
const md = fs.readFileSync(file, 'utf8');
const dict = parseChunks(md, file);
for (const [chunk, code] of Object.entries(dict)) {
const name = `${file}__${chunk}.ts`;
module: ts.ModuleKind.CommonJS,
const cache: Record<string, Record<string, string>> = {};
const host = ts.createCompilerHost(options);
function getChunkCode(p: string, c: string) {
const mdText = fs.readFileSync(p, 'utf8');
cache[p] = parseChunks(mdText, p);
host.getSourceFile = (f, l) => {
if (f === name) return ts.createSourceFile(f, code, l);
const m = /(.*\.ts\.md)__(.+)\.ts$/.exec(f);
const chunkCode = getChunkCode(m[1], m[2]);
if (chunkCode) return ts.createSourceFile(f, chunkCode, l);
return ts.createSourceFile(f, fs.readFileSync(f, 'utf8'), l);
if (f === name) return code;
const m = /(.*\.ts\.md)__(.+)\.ts$/.exec(f);
const chunkCode = getChunkCode(m[1], m[2]);
if (chunkCode) return chunkCode;
return fs.readFileSync(f, 'utf8');
host.fileExists = (f) => {
if (f === name) return true;
const m = /(.*\.ts\.md)__(.+)\.ts$/.exec(f);
const chunkCode = getChunkCode(m[1], m[2]);
return chunkCode !== undefined;
host.resolveModuleNames = (mods, containing) =>
const info = resolveImport(n, file);
resolvedFileName: `${info.absPath}__${info.chunk}.ts`,
extension: ts.Extension.Ts,
const res = ts.resolveModuleName(
return res as ts.ResolvedModule;
const program = ts.createProgram([name], options, host);
const extra = ts.getPreEmitDiagnostics(program).map((d) => {
const sf = program.getSourceFile(name);
message: ts.flattenDiagnosticMessageText(d.messageText, '\n'),
start: sf?.getLineAndCharacterOfPosition(d.start ?? 0) ?? {
end: sf?.getLineAndCharacterOfPosition(
(d.start ?? 0) + (d.length ?? 0),
result[file] = diags as TsMdDiagnostic[];