Привет, Хабр! Меня зовут Дима, я Head of Frontend в Dodo Engineering. Моя команда создаёт инструменты для удобной работы с фронтендами, унифицирует подходы к разработке, помогает другим командам в создании удобных пользовательских интерфейсов Dodo IS.

Недавно мне срочно понадобилось написать плагин для TypeScript. Начал я с того, что погуглил, как это сделать. По пути боролся с повышенным потреблением памяти и искал недостающие файлы в массиве, переписывал Proxy и не только, а закончил на... А впрочем это вы узнаете в конце.

Чтобы засинхрониться, оставляю вам в начале статьи ссылку на код плагина. Открывайте его и давайте вместе разбираться, зачем мне вообще понадобился плагин и как я его писал.

Почему решил написать?

TypeScript — странный язык. Он помогает разработчикам, но при этом вызывает много вопросов. Он добавляет статическую типизацию в JavaScript, что позволяет IDE заранее «ловить» ошибки и подсказывать разработчику, как лучше писать код. Это делает код более предсказуемым и помогает избежать множества багов на этапе написания.

Однако у TypeScript есть обратная сторона. Строгая типизация усложняет написание кода, особенно тем, кто привык к гибкости JavaScript. Порой описание типов занимает больше времени, чем решение задачи. Некоторые разработчики осознанно отказываются от TypeScript в своих проектах по этой причине.

Кроме того, TypeScript — это как дополнительный слой в разработке. Сам по себе он не может работать в браузере или на сервере, его сначала нужно преобразовать в обычный JavaScript. И хотя TypeScript помогает писать более чистый код, в итоге весь процесс выглядит лишним шагом, причём не всегда оправданным.

Тем не менее TypeScript в каком-то смысле является дополнением к тестам и документации, особенно в больших проектах, но только если его правильно настроить. Одна из самых важных настроек в TypeScript — опция strict. Она заслуживает особого внимания, когда речь идёт о масштабных проектах. Эта опция включает в себя несколько других.

strictNullChecks

Одна из самых главных опций. Без неё все | null | undefined (nullable) типы просто игнорируются:

function findUserName(): string | null {
  return null; // здесь может быть обращение к бэкенду
}

const userName = getUserName();

console.log(userName.toUpperCase()); // без включённого strict мы не увидим ошибку в IDE, хотя userName может оказаться null. А вот в рантайме упадём с ошибкой

noImplicitAny

function stringToUpper(name) { // с включённым strict TypeScript не позволит передать любой аргумент в name, запретит неявный any-тип
  return name.toUpperCase();
}

stringToUpper(42); // здесь мы упадём с ошибкой в рантайме

Существует ряд других опций, которые также критичны, но требуют более детального рассмотрения. Чтобы не увеличивать объём статьи, я приведу их список со ссылками на официальную документацию:

Но что делать, если на этапе зарождения проекта strict не был включён? Ведь по началу может казаться, что это лишняя нагрузка: проект небольшой, всё под контролем, и строгая типизация кажется избыточной. К тому же переводить проект с JavaScript на TypeScript намного проще с выключенным strict — просто переименовать .jsфайлы в .ts.

Но когда проект вырастает до значительных размеров, исправлять эти ошибки становится всё сложнее. И если команда решит на каком-то этапе включить strict, она столкнётся с тем, что нужно переписывать огромные участки кода, чтобы удовлетворить требованиям строгой типизации. Это может сильно затормозить разработку и усложнить поддержку кода.

С такой проблемой я и столкнулся на одном из проектов. Несколько лет назад мы перевели его с JavaScript на TypeScript, не включив опцию strict. Возможно, мы сделали это случайно — в пустом tsconfig она отключена по умолчанию, а возможно осознанно — из-за описанных выше причин. Так или иначе, команда начала сталкиваться с проблемами — в проде начали вылезать ошибки, связанные со strictNullChecks. Дело было в переменных. В коде они были нулевыми, но в какой-то момент nullable терялся, так как IDE и TypeScript никак об этом не предупреждали.

В проекте больше 3000 файлов. Простое включение strict подсветило сотни ошибок. Их исправление заняло бы недели, а может и месяцы. Возник вопрос: а как перевести только часть проекта, содержащую критичную функциональность? К сожалению, TypeScript не позволяет переопределять tsconfig для определённых папок, так как он воспринимает весь код как целостный проект, где друг с другом связан каждый файл и каждый тип. У TypeScript есть issue на эту тему, открытый в 2019 году.

Одно из решений: раздробить один монолит-проект на мелкие части, workspaces. И уже в этих частях внедрять strict. Но из-за тесной связанности компонентов проекта, эта задача заняла бы ещё больше времени. Хоть мы и смогли собрать низко висящие фрукты — выделили независящие от других частей проекта утилиты и UI-компоненты — этого всё равно было мало. Оставалась потребность переопределять tsconfig только для определённых частей проекта и постепенно переводить на strict.

В какой-то момент я вспомнил о возможности писать свои плагины для TypeScript. И пошёл разбираться…

Плагины для IDE

По запросу «TypeScript plugin» в гугле, мы попадаем на официальный гайд по написанию плагина. Это не самый подробный гайд. Он задаёт только базовый вектор в создании плагинов. Поэтому я как слепой котёнок вооружился исходниками TypeScript, IDE, и пошёл писать плагин.

Быстрый набросок плагина

Первым делом я создал два workspace. В одном из них находится код моего плагина (packages/plugin), а в другом (packages/example) — этот плагин подключается как обычный npm-пакет. Весь код плагина содержится в index.ts файле и собирается обычной командой tsc. В example добавляем в tsconfig следующие настройки:

{
  "compilerOptions": {
    "plugins": [
      {
        "name": "ts-plugin", // название workspace в packages/plugin/package.json
      },
    ]
  },
}

Самый простой код плагина может выглядеть так:

import type ts from 'typescript/lib/tsserverlibrary';

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({
    create: pluginCreateInfo =>
        new Proxy(pluginCreateInfo.languageService, {
            get: (target, property: keyof ts.LanguageService) => {
                if (property === `getSemanticDiagnostics`) {
                    return (fileName: string) => {
                        const originalDiagnostic = target.getSemanticDiagnostics(fileName);

                        return originalDiagnostic.map(diagnostic => ({
                            ...diagnostic,
                            messageText: `Привет, Хабр! ${diagnostic.messageText}`,
                        }));
                    };
                }

                return target[property];
            },
        }),
});

export = plugin;

По итогу получаем следующий результат:

Здесь и далее я использую Proxy для модификации оригинального languageService. Однако можно воспользоваться и «классическим» копированием объекта. Оно описано в официальном гайде.

Разбор основных параметров

Разберём подробнее приведённый выше код, чтобы узнать, какие функции нам доступны:

  • ts.server.PluginModuleFactory — функция, аргумент которой содержит текущий экземпляр typescript. Он нам понадобится в дальнейшем. Возвращать функция должна объект со следующими ключами:

  • getExternalFiles— необязательная функция. Она возвращает список файлов, которые плагин хочет рассматривать в процессе работы. Они могут и не быть частью проекта TypeScript. Например, если мы пишем плагин для vue, все файлы vue должны быть включены в проект;

  • onConfigurationChanged — необязательная функция. Вызывается, когда изменяется конфигурация плагина. Когда TypeScript сервер получает новую конфигурацию через файл tsconfig.json, эта функция может реагировать на изменения настроек и применять их к работе плагина;

  • create — обязательная функция и самая важная для разработчика плагина. Её аргумент — объект PluginCreateInfo. Его мы разберём позже. Функция должна вернуть LanguageService— это основной объект, с которым мы будем работать. Он предоставляет основные инструменты для работы с кодом: автодополнение, навигацию по коду (go to definition), показ ошибок, показ типов и документацию;

  • PluginCreateInfo содержит следующие ключи:

  • project — предоставляет доступ к данным текущего проекта, включая список файлов, информацию о зависимостях и другие проектные метаданные. Плагин может использовать эту информацию, чтобы адаптироваться к структуре проекта и выполнять операции, требующие понимания проектной организации;

  • languageService — оригинальный LanguageService был описан выше;

  • languageServiceHost — предоставляет информацию, необходимую для работы languageService. С его помощью плагин может запрашивать файлы, настройки компиляции, а также доступ к синтаксическим и семантическим данным проекта;

  • serverHost — предоставляет доступ к низкоуровневым операциям с файловой системой и проектом. Плагин может использовать его для чтения файлов, управления директориями и выполнения других операций с файловой системой, которые требуются для работы плагина;

  • session — это ключ, который предоставляет доступ к сессии сервера TypeScript. Сессия отслеживает текущее состояние работы редактора с проектом, управляет запросами между IDE и сервером TypeScript. Она позволяет плагину взаимодействовать с редактором и использовать такие функции, как регистрация команд или расширение возможностей обработки запросов;

  • config — содержит пользовательские настройки плагина, которые могут быть переданы через tsconfig.json. Эти параметры позволяют гибко настроить поведение плагина в зависимости от потребностей конкретного проекта.

Начинаем писать плагин для переопределения tsconfig.json

Из всего многообразия доступных параметров нам нужны: typescript из аргумента функции плагина, languageService, languageServiceHost и config из функции create.

Основная наша задача — создать отдельные languageService со своими настройками для каждой переопределённой папки.

Начнём с простого — определим настройки для нашего плагина. Это будет простой интерфейс:

import type ts from 'typescript/lib/tsserverlibrary';

export interface Override {
    files: string[];
    compilerOptions: ts.server.protocol.CompilerOptions;
}

export interface PluginConfig {
    overrides: Override[];
}

ts.server.protocol.CompilerOptions — это любые настройки, доступные в tsconfig.json

Передаём эти настройки в tsconfig.json:

{
  "compilerOptions": {
    "strict": false,
    "plugins": [
      {
        "name": "ts-overrides-plugin",
        "overrides": [
          {
            "files": ["src/modern/**/*.{ts,tsx}"],
            "compilerOptions": {
              "strict": true,
            },
          },
        ]
      },
    ]
  },
}

Для определения путей я решил использовать glob-паттерн. Для разработчиков он более привычен как по самому tsconfig (поля files, includes, exclude, etc), так и по eslint.config.

Теперь мы можем получить эти настройки внутри плагина:

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({
    create: info => {
        const { overrides } = info.config as PluginConfig;

        console.log(overrides);

        return info.languageService;
    },
});

export = plugin;

Далее нам нужно создать отдельный languageService для каждой папки, требующей переопределения настроек TypeScript. Создадим функцию getOverrideLanguageServices и подробно её разберём:

import outmatch from 'outmatch';

const getOverrideLanguageServices = (
    typescript: typeof ts,
    overrides: Override[],
    languageServiceHost: ts.LanguageServiceHost,
): ts.LanguageService[] =>
    [...overrides].reverse().map(override => {
        const overrideLanguageServiceHost: ts.LanguageServiceHost = {
            fileExists: path => !!languageServiceHost.fileExists?.(path),
            getCurrentDirectory: (): string => languageServiceHost.getCurrentDirectory(),
            getDefaultLibFileName: (options: ts.CompilerOptions): string =>
                languageServiceHost.getDefaultLibFileName(options),
            getScriptSnapshot: fileName => languageServiceHost.getScriptSnapshot(fileName),
            getScriptVersion: fileName => languageServiceHost.getScriptVersion(fileName),
            readFile: (path, encoding) => languageServiceHost.readFile?.(path, encoding),
            getCompilationSettings: () => ({
                ...languageServiceHost.getCompilationSettings(),
                ...typescript.convertCompilerOptionsFromJson(
                    override.compilerOptions,
                    languageServiceHost.getCurrentDirectory(),
                ).options,
            }),
            getScriptFileNames: () => {
                const originalFiles = languageServiceHost.getScriptFileNames();
                const isMatch = outmatch(override.files);

                return originalFiles.filter(fileName =>
                    isMatch(relative(languageServiceHost.getCurrentDirectory(), fileName)),
                );
            },
        };

        return typescript.createLanguageService(overrideLanguageServiceHost);
    });

Функция принимает параметры:

  • typescript. Он нужен нам для создания экземпляра languageService;

  • overrides — массив наших переопределений. Для каждого мы создадим отдельный languageService;

  • languageServiceHost. Он нужен для создания languageService. Как упоминалось выше, он позволит нам обращаться к файлам проекта и его конфигурации. С его помощью мы создадим собственныйlanguageServiceHost, в котором переопределим функции для получения конфигурации и списка файлов проекта.

Для начала сделаем reverse массива overrides. Это нужно для того, чтобы при поиске через find мы получали последний languageService, способный обработать файл на наличие ошибок (по аналогии с eslint).

Далее для каждого override создадим languageServiceHost, определив обязательные методы:

  • fileExists должен вернуть при вызове true, если файл существует. Здесь, как и много где дальше, мы можем просто вызвать метод из оригинального languageServiceHost. Стоит учесть и то, что в typescript ≤ 4 этот метод был необязательным. Поэтому при вызове используем optional chaining;

  • getCurrentDirectory получает текущую директорию проекта (корневую папку). Используем оригинальный метод;

  • getDefaultLibFileName возвращает имя файла библиотеки по умолчанию (обычно lib.d.ts). Используется для определения файла стандартной библиотеки TypeScript, который подключается к проекту. Используем оригинальный метод;

  • getScriptSnapshot возвращает снимок текущего состояния файла. Снимки используются TypeScript для анализа изменений в коде без необходимости перечитывать файл каждый раз. Используем оригинальный метод;

  • getScriptVersion возвращает текущую версию скрипта (файла), по которой TypeScript может определить, изменился ли файл. Эта строка позволяет TypeScript определять, изменился ли файл, и нужно ли его анализировать повторно;

  • readFile читает содержимое файла по указанному пути path с возможностью указать кодировку файла (encoding). Как и в случае с fileExists, здесь используется безопасный вызов с проверкой на наличие метода, так как в typescript ≤ 4 этот метод был необязательным. Используем оригинальный метод;

  • getCompilationSettings возвращает объект настроек компиляции TypeScript (ts.CompilerOptions). Этот метод объединяет настройки, которые предоставляет оригинальный languageServiceHost, с нашими настройками компиляции из объекта override.compilerOptions. По сути, здесь происходит основная «магия» нашего плагина. Метод использует функцию convertCompilerOptionsFromJson, которая преобразует JSON-объект с настройками в валидные параметры компилятора TypeScript. Например, поле module, которое можно задать разными способами (CommonJS, commonjs), он преобразует в число;

  • getScriptFileNames возвращает массив имён всех файлов скриптов в проекте. Сначала вызывается оригинальный метод getScriptFileNames из languageServiceHost, который возвращает список всех файлов, известных TypeScript. Затем с помощью функции outmatch проверяется, какие из этих файлов соответствуют паттернам, указанным в override.files. Фильтрация позволяет плагину работать только с файлами, которые соответствуют этим паттернам. Вместо outmatch можно было использовать любую другую библиотеку для работы с glob-паттернами.

На основе созданного объекта overrideLanguageServiceHost, мы создаём languageService с помощью typescript.createLanguageService(overrideLanguageServiceHost). Далее именно его мы будем вызывать для файлов, в которых нужно переопределить настройки.

Определим функцию для получения languageService по имени файла:

const getLanguageServiceForFile = (
    fileName: string,
    overrideLanguageServices: ts.LanguageService[],
    originalLanguageService: ts.LanguageService,
): ts.LanguageService => {
    const overrideServiceForFile = overrideLanguageServices.find(
        override => override.getProgram()?.getRootFileNames().includes(fileName),
    );

    if (overrideServiceForFile) {
        return overrideServiceForFile;
    }

    return originalLanguageService;
};

Здесь мы проходимся по массиву созданных выше languageService и ищем тот, что готов проанализировать файл. Если не находим, берём оригинальный. На этом почти всё. Нам осталось только определить основную функцию плагина:

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({
    create: info => {
        const { overrides } = info.config as IdePluginConfig;

        const overrideLanguageServices = getOverrideLanguageServices(typescript, overrides, info.languageServiceHost);

        return new Proxy(info.languageService, {
            get: (target, property: keyof ts.LanguageService) => {
                if (property === `getSemanticDiagnostics`) {
                    return (fileName: string) => {
                        const overrideForFile = getLanguageServiceForFile(fileName, overrideLanguageServices, target);

                        return overrideForFile.getSemanticDiagnostics(fileName);
                    };
                }

                return target[property];
            },
        });
    },
});


export = plugin;

Здесь мы переопределяем оригинальный метод getSemanticDiagnostics, который возвращает диагностику по файлу. В нём мы получаем переопределённый languageService, созданный нами, и проводим с его помощью диагностику. По итогу в одном проекте мы видим разный вывод ошибок для разных папок:

Устраняем проблемы

Плагин выполняет свою основную задачу — с его помощью мы получаем разную диагностику для разных файлов. Он был написан на коленке, а потому с ним возникли некоторые проблемы. Разберём их подробнее.

Память

Взглянув на код, можно определить источник теоретических проблем — повышенное потребление памяти. Мы создаём несколько languageServiceHost и languageService, каждый из которых хранит состояние проекта: информацию о типах, содержимое файлов и т.д. На большом проекте и с большим количеством overrides потребление памяти может сильно возрасти.

Как сгладить ситуацию? Например, с помощью typescript.createDocumentRegistry(). Он возвращает DocumentRegistry, который может хранить состояние файлов, их версии, выгружать неиспользуемые, а самое главное — шарить это состояние между разными экземплярами languageService. К сожалению, мы не можем получить DocumentRegistry из оригинальногоlanguageService, поэтому создадим новый и будем использовать его для всех наших languageService:

const getOverrideLanguageServices = (
    typescript: typeof ts,
    overridesFromConfig: Override[],
    languageServiceHost: ts.LanguageServiceHost,
    docRegistry: ts.DocumentRegistry,
): ts.LanguageService[] =>
    [...overridesFromConfig].reverse().map(override => {
        // ...
        return typescript.createLanguageService(overrideLanguageServiceHost, docRegistry);
    });

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({
    create: info => {
        // ...
        const docRegistry = typescript.createDocumentRegistry();

        const overrideLanguageServices = getOverrideLanguageServices(
            typescript,
            overrides,
            info.languageServiceHost,
            docRegistry,
        );

        const originalLanguageServiceWithDocRegistry = typescript.createLanguageService(
            info.languageServiceHost,
            docRegistry,
        );
        //...
    },
});

Таким образом, мы сильно оптимизируем работу с памятью.

Глобальные типы

При тестировании на реальном проекте вскрылась проблема — TypeScript начал ругаться на отсутствие глобальных типов, описанных в d.ts файлах. Ошибка была в создании LanguageServiceHost, в формировании массива файлов:

getScriptFileNames: () => {
    const originalFiles = project.getScriptFileNames();
    const isMatch = outmatch(override.files);

    return originalFiles.filter(fileName => isMatch(relative(project.getCurrentDirectory(), fileName)));
},

Здесь мы не учитываем d.ts файлы, если они явно не указаны в override.files. Проблема решается их явным добавлением в массив:

getScriptFileNames: () => {
    const originalFiles = project.getScriptFileNames();
    const isMatch = outmatch(override.files);

    return originalFiles.filter(
        fileName =>
            fileName.endsWith(`.d.ts`) || isMatch(relative(project.getCurrentDirectory(), fileName)),
    );
}

Подсветка типов

Мы переопределили только метод getSemanticDiagnostics, который выводит ошибки. Но забыли про подсказки о типах. Например, создадим файл legacy.ts, в котором strict выключен:

Из-за отсутствия strictNullChecks, итоговый тип переменной из string | undefined превращается в string. Импортируем эту переменную из другого файла modern.ts, где strict включён:

Ошибка отображается верно, но тип переменной всё ещё остаётся string. Чтобы это исправить, переопределим ещё один метод в languageService:

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({
    create: info => {
        // ...

        return new Proxy(originalLanguageServiceWithDocRegistry, {
            get: (target, property: keyof ts.LanguageService) => {
                if (property === `getQuickInfoAtPosition`) {
                    return ((fileName: string, position: number) => {
                        const overrideForFile = getLanguageServiceForFile(fileName, overrideLanguageServices, target);

                        return overrideForFile.getQuickInfoAtPosition(fileName, position);
                    });
                }

                // ...

                return target[property as keyof ts.LanguageService];
            },
        });
    },
});

Теперь и ошибка, и тип отображается корректно:

Дебаггинг

Поскольку плагин работает в IDE, обычный console.log или точка остановы не помогут нам в его отладке. Можно использовать 2 решения для отладки:

  • писать логи в файл средствами nodejs;

  • использовать info.project.projectService.logger.info, запустить VSCode или WebStorm в режиме отладки и читать логи также из файла.

Совместимость с разными версиями TypeScript

Изначально я писал плагин только под версию TypeScript 5.3 и использовал Proxy для создания не только languageService, но и languageServiceHost — всё работало хорошо. В обновлении TypeScript 5.4 разработчики изменили поведение languageServiceHostи при использовании плагина любые ошибки типов перестали выводиться. Почитав исходники TypeScript, я нашёл участок кода, который косвенно запрещает использование одного экземпляра languageServiceHostнесколькими languageService. Это сделано с целью оптимизации. Поэтому я переписал Proxy на создание нового объекта с нуля.

Как было описано ранее, нужно быть осторожным с более старыми версиями TypeScript — некоторые методы там отсутствовали или были опциональными.

В целом плагин, который вмешивается в стандартное поведение TypeScript, достаточно шаткая история. Каждое изменение версии TypeScript требует отладки.

Неправильное понимание Program

Изначально я пытался создать ts.Program с помощью typescript.createProgram(), передав в него переопределённые настройки, а не создавать новые languageService. Далее в методе getSemanticDiagnosticsя пытался вызывать методы из созданной ts.Program. Что-то вроде этого:

const plugin: ts.server.PluginModuleFactory = ({ typescript }) => ({
    create: info => {
        // ...

        const newProgram = typescript.createProgram([`modern.ts`], {
            ...defaultOptions,
            ...overrideOptions,
        });

        return new Proxy(info.languageService, {
            get: (target, property: keyof ts.LanguageService) => {
                if (property === `getSemanticDiagnostics`) {
                    return ((fileName: string) => {
                        // ...
                        const sourceFile = newProgram.getSourceFile(fileName);

                        return newProgram.getSemanticDiagnostics(sourceFile);
                    });
                }

                return target[property];
            },
        });
    },
});

Но этот способ переставал работать при первом же изменении файла, расположение диагностики уезжало:

https://lh7-rt.googleusercontent.com/slidesz/AGV_vUf57SJodPZu6IW5DM_yDjBlN6RKPo0JMJUycxyvurtrQbrEVXdcmzCc4_7ahI4Gmc4eMzIiO69_yMI_7ePY2JG6E2jph4FR1xGhYvmV67I_ovYH8ZZuDHSJtdtdR49HHstBAd_yUa_wYOFp6IeYn5_1SqEgMlM=s2048?key=ZBKvTn_p51lwBzt4p8OcNg

Дело в том, что typescript.createProgram() создаёт «слепок» всего проекта на момент создания. Он не предназначен для использования в реальном времени с постоянно меняющимися файлами. Можно бы было создавать новый ts.Program при каждом вызове getSemanticDiagnosticsили при изменении файлов, но такой способ оказался очень ресурсозатратным и не нативным.

Бонус. Игнорирование файлов из проверки типов

Существуют редкие ситуации, когда нам нужно исключить файл из проверки типов. Например, это может быть JS-код, который нужно переписать на TypeScript. Если файл один — это не проблема. Но если это целая папка, в некоторых файлах захочется временно отключить проверку типов.

TypeScript позволяет заглушать ошибки для конкретных строк (// @ts-ignore) и для всего файла (// ts-nocheck). Но такие комментарии будут разбросаны по файлам — их легко потерять. С помощью плагина можно держать такие исключения в одном месте, в tsconfig.json:

"compilerOptions": {
    "plugins": [
      {
        "name": "ts-overrides-plugin",
        "ignores": ["src/ignored/**/*.{ts,tsx}"]
    ]
  }

Что касается реализации — к вышеупомянутому коду добавляется пара строк:

const { overrides, ignores } = info.config as IdePluginConfig;
const ignoresMatcher = ignores ? outmatch(ignores) : null;

// ...

if (property === `getSemanticDiagnostics`) {
    return (fileName => {
        if (ignoresMatcher?.(relative(info.project.getCurrentDirectory(), fileName))) {
            return [];
        }

        const overrideForFile = getLanguageServiceForFile(fileName, overrideLanguageServices, target);

        return overrideForFile.getSemanticDiagnostics(fileName);
    }) as ts.LanguageService['getSemanticDiagnostics'];
}

Что же дальше?

А дальше мы запускаем команду tsc и видим, что наш плагин не работает. И это ожидаемо — плагин предназначен только для IDE.

Мы уже можем отлавливать новые ошибки на этапе разработки, но на этапе транспиляции они всё ещё могут проскользнуть. Нас это не устраивает. А написание плагина для транспиляции — это совершенно другой мир со своими тонкостями и костылями. О нём я расскажу в следующей статье.

P.S. А с кодом плагина можно можно ознакомиться по ссылке.

Комментарии (0)