[Из песочницы] Typescript Compiler API: возьми управление компилятором в свои руки

hotocr4gcgj_he5xhhf6asthb6g.jpeg

В разработке приложений на Typescript всегда есть этап сборки проекта. Обычно для этого используются системы сборки и автоматизации workflow, такие как webpack или gulp, обвешанные достаточным количеством плагинов, либо процесс сборки размазывается в командах package.json и шелл-скриптах с использованием нативного tsc или команд CLI используемого в проекте фреймворка. Все эти решения имеют свои плюсы и минусы. Зачастую в процессе сборки нужно сделать что-то нестандартное, и оказывается, что используемая система сборки не предоставляет нужную функциональность из коробки, а имеющиеся плагины делают не совсем то, что надо. В такие моменты работа над проектом встает, и начинается судорожное ковыряние в конфигах и поиск подходящего плагина. В какой-то момент понимаешь, что за время, потраченное на поиск подходящего костыля, можно было написать свое решение.

Во многих случаях критичные процессы в проекте можно автоматизировать скриптами на javascript, выразительность и функциональность которого вполне позволяет описать нужный workflow и выбирать из всего разнообразия библиотек, не заморачиваясь наличием для них плагинов под конкретную систему сборки. Важное преимущество такого подхода — полный контроль над процессами и максимальная гибкость. Для проектов, в которых используется Typescript в качестве основного языка разработки, возникает вопрос, как встроить процесс его компиляции в свой workflow. Здесь на помощь приходит Typescript Compiler API. В этой статье мы посмотрим, как его можно использовать для того, чтобы выполнить компиляцию проекта, реализованного на Typescript, взаимодействуя с компилятором на разных этапах его работы и напишем скрипт для hot-reloading«а REST-сервера, разработанного на Nest.js.
У меня есть проект на ноде, над которым я работаю в свободное время. Изначально он предназначался для небольшого бизнеса, который так и не взлетел, а теперь я использую его как полигон для своих экспериментов. Проект, который изначально строился на Nuxt.js в связке с Fastify, пережил множество трансформаций. Здесь и переход на typescript и преобразование в монорепозиторий, и замена Fastify на Nest.js. Поскольку проект небольшой и серьезной нагрузки не предполагалось, все это должно было крутится на одном сервере, поэтому я использовал единый инстанс Express как для REST сервера так для отдачи фронта через Nuxt.js. Для этого Express, который работает под капотом Nest.js, получает рендер-функцию Nuxt.js.

Это решение хорошо работает в продакшене, но мне, поскольку я работал над проектом один, было удобно параллельно писать функции API и делать интерфейс. Для это перезапуск Nest.js при изменении файлов не должен приводить к перезапуску работающего в режиме HMR Nuxt.js, иначе он начинал процесс сборки заново, что лишало смысла всю затею. Но тут возникла проблема, дело в том, что хотя в CLI Nest.js есть режим watch, который умеет делать hot-reload, загружает он его в отдельном процессе, который перезапускается при перекомпиляции проекта. Из-за этого каждый перезапуск сервера в таком режиме будет приводить к потере контекста для Nuxt и затем будет запускаться его полная сборка.

После того, как все это выяснилось, я решил сделать в проекте свой hot-reload. Первоначально я попробовал реализовать решение с использованием webpack, как предлагается в документации Nest.js, но оказалось, что start-server-webpack-plugin, который там используется для реализации перезапуска использовал ту же стратегию, что CLI, т.е. запускал его в отдельном процессе и перезапускал по необходимости. Этот плагин я переписал, чтобы он работал как нужно мне, но с использованием Webpack решение получилось довольно тяжеловесно и не без проблем, которые выплывали тут и там, мешая сосредоточится на разработке проекта.

Тогда я решил, что надо менять подход, мне хотелось напрямую использовать механизмы, заложенные в компилятор tsc, который всегда работал как часы быстро и стабильно. В итоге я нашел хорошую статью, в которой были примеры использования TypeScript Compiler API. Оказалось, что инструменты, встроенные в библиотеку Typescript позволяют реализовать нужные мне функции и достаточно удобны в применении. К сожалению, я столкнулся с тем, что по Typescript Compiler API очень мало информации, поэтому решил поделится тем, что мне удалось узнать. Отмечу также что данный API на момент написания статьи находится в процессе разработки и некоторые моменты со временем могут поменяться.

Простая компиляция


Чтобы разобраться с принципами работы TypeScript Compiler API для начала попробуем просто выполнить с его помощью компиляцию проекта. Проект, на котором я буду экспериментировать, имеет стандартную для nest.js структуру: исходники расположены в папке src, точка входа — файл ./src/main.ts.

Для того, чтобы компилятор успешно скомпилировал проект, ему надо передать параметры компиляции. Самый простой способ, это захаркодить его прямо в скрипте. Создадим файл tsconfig.js:

const {ModuleResolutionKind, ModuleKind, ScriptTarget} = require("typescript")

module.exports =  {
    moduleResolution: ModuleResolutionKind.NodeJs,
    module: ModuleKind.CommonJS,
    target: ScriptTarget.ES2019,
    declaration: true,
    removeComments: true,
    emitDecoratorMetadata: true,
    experimentalDecorators: true,
    sourceMap: true,
    outDir: "./dist",
    baseUrl: "./",
    allowJs: true,
    skipLibCheck: true,
  }


Обратите внимание, что некоторые параметры, такие как moduleResolution, module, target определяются через перечисления, поэтому напрямую забрать параметры из tsconfig.json, где они прописаны в виде строк не выйдет, так что для реализации простейшего примера я его использовать не буду. Как корректно забрать параметры из tsconfig.json мы разберемся немного позже.

Теперь у нас есть параметры компиляции и можно перейти к следующему шагу. Для этого создадим файл ts-compiler.js со следующим содержимым:

const ts = require('typescript');

function compile() {
  const compilerOptions = require('./tsconfig');
  const program = ts.createProgram(['./src/main.ts'], compilerOptions);
  const emitResult = program.emit();
}

compile();


Если теперь запустить этот скрипт на исполнение, в директории ./dist должны появится скомпилированные файлы программы. Компиляция происходит в два этапа: сначала команда createProgram анализирует исходные файлы и создает список файлов для компиляции, после чего выполняем компиляцию и сохранение скомпилированных файлов при помощи функции emit. Довольно просто.

Для повышения удобства использования теперь надо реализовать загрузку параметров компиляции из файла tsconfig.json. Добавим в скрипт следующую функцию:


const formatHost = {
  getCanonicalFileName: path => path,
  getCurrentDirectory: ts.sys.getCurrentDirectory,
  getNewLine: () => ts.sys.newLine,
};

function getTSConfig() {
  const configPath = ts.findConfigFile('./', ts.sys.fileExists, 'tsconfig.json');
  const readConfigFileResult = ts.readConfigFile(configPath, ts.sys.readFile);
  if (readConfigFileResult.error) {
    throw new Error(ts.formatDiagnostic(readConfigFileResult.error, formatHost));
  }
  const jsonConfig = readConfigFileResult.config;
  const convertResult = ts.convertCompilerOptionsFromJson(jsonConfig.compilerOptions, './');
  if (convertResult.errors && convertResult.errors.length > 0) {
    throw new Error(ts.formatDiagnostics(convertResult.errors, formatHost));
  }
  const compilerOptions = convertResult.options;
  return compilerOptions;
}


Для работы с файлами конфигурации API предоставляет ряд функций, которые позволяют найти, загрузить и преобразовать конфигурацию в тот вид, который понимает компилятор. Здесь используется функция findConfigFile чтобы определить путь к файлу конфигурации, потом он загружается как json при помощи readConfigFile, и далее, если нет ошибок, при помощи convertCompilerOptionsFromJson получаем параметры компиляции.

В случае, если при загрузке конфига что-то пошло не так, наша функция генерирует исключения, используя механизмы передачи сообщений об ошибках, заложенные в API. Все сообщения об ошибках и предупреждения библиотека возвращает в виде объектов класса Diagnostic. Чтобы на их основе сформировать сообщение для вывода в консоль, можно использовать стандартные функции форматирования библиотеки, в данном случае это formatDiagnostic для одного сообщения и formatDiagnostics для массива. Для корректной работы функций форматирования требуется объект formatHost, который содержит важные для форматирования сообщения параметры.

Теперь, чтобы получить параметры компилятора, мы можем вызвать в нашем скрипте функцию getTSConfig.

function compile() {
  const compilerOptions = getTSConfig();
…
}


На данном этапе, если компилятор найдет ошибки в коде, мы об этом не узнаем, давайте исправим это. Все ошибки в коде, которые были обнаружены на этапе компиляции функция emit возвращает в поле diagnostcs объекта EmitResult. Для красивого отображения ошибок в коде можно использовать функцию formatDiagnosticsWithColorAndContext. Также имеет смысл проверить ошибки, обнаруженные на этапе создания программы. Для этого используем функцию getPreEmitDiagnostics. Дополним наш скрипт следующим образом:

function compile() {
  const compilerOptions = getTSConfig();
  const program = ts.createProgram(['./src/main.ts'], compilerOptions);
  console.log(
    ts.formatDiagnosticsWithColorAndContext(
      ts.getPreEmitDiagnostics(program),
      formatHost,
    ),
  );
  const emitResult = program.emit();
  console.log(
    ts.formatDiagnosticsWithColorAndContext(emitResult.diagnostics, formatHost),
  );
  return emitResult.diagnostics.length === 0;
}


Если теперь внести в наши исходники ошибку, получим в консоли красиво отформатированное сообщение:

gi9_ck4zm0cjrg1mx8tzxbunkek.png

Итак, мы получили скрипт, который будет компилировать наши исходники и отображать в консоли ошибки в случае их возникновения. Как видите, такой подход дает возможность очень гибко реализовать процесс компиляции, поскольку у нас есть доступ ко всем этапам и результатам компиляции. Дополнительной гибкости можно достичь, кастомизировав CompilerHost, который содержит необходимый компилятору набор функций. Допустим, мы захотели отображать в консоли пути к файлам, которые читает компилятор в процессе работы. Это несложно сделать, передав в функцию createProgram кастомизированный CompilerHost. Посмотрим, как это работает на практике.

Я не хочу засорять вывод портянкой из сотен строк путей к файлам, поэтому напишу функцию, которая будет выводить пути в одной строчке:

function displayFilename(originalFunc, operationName) {
  let displayEnabled = false;
  function displayFunction() {
    const fileName = arguments[0];
    if (displayEnabled) {
      process.stdout.clearLine();
      process.stdout.cursorTo(0);
      process.stdout.write(`${operationName}: ${fileName}`);
    }
    return originalFunc(...arguments);
  }
  displayFunction.originalFunc = originalFunc;
  displayFunction.enableDisplay = () => {
    if (process.stdout.isTTY) {
      displayEnabled = true;
    }
  };
  displayFunction.disableDisplay = () => {
    if (displayEnabled) {
      displayEnabled = false;
      process.stdout.clearLine();
      process.stdout.cursorTo(0);
    }
  };
  return displayFunction;
}


Теперь дополним код функции compile ().

function compile() {
  const compilerOptions = getTSConfig();
  const compilerHost = ts.createCompilerHost(compilerOptions);
  compilerHost.readFile = displayFilename(compilerHost.readFile, 'Reading');
  compilerHost.readFile.enableDisplay();
  const program = ts.createProgram(
    ['./src/main.ts'],
    compilerOptions,
    compilerHost,
  );
  compilerHost.readFile.disableDisplay();

  console.log(
    ts.formatDiagnosticsWithColorAndContext(
      ts.getPreEmitDiagnostics(program),
      formatHost,
    ),
  );

  compilerHost.writeFile = displayFilename(compilerHost.writeFile, 'Emitting');
  compilerHost.writeFile.enableDisplay()
  const emitResult = program.emit();
  compilerHost.writeFile.disableDisplay();
  console.log(
    ts.formatDiagnosticsWithColorAndContext(emitResult.diagnostics, formatHost),
  );
  return emitResult.diagnostics.length === 0;
}


Итак, теперь мы создаем хост с помощью функции createCompilerHost и заменяем нашей реализацией функции readFile и writeFile. Также функция compile () возвращает истину, если после окончания работы не обнаружено ошибок. Теперь в случае, если компиляция прошла без ошибок можно сразу запустить скомпилированный сервер. Посмотрим, что получилось:

istjrvmrrq-htnitutw8_p_ux64.gif

Вот полный код получившегося скрипта:

ts-compiler.js
const ts = require('typescript');

const formatHost = {
  getCanonicalFileName: path => path,
  getCurrentDirectory: ts.sys.getCurrentDirectory,
  getNewLine: () => ts.sys.newLine,
};

function getTSConfig() {
  const configPath = ts.findConfigFile(
    './',
    ts.sys.fileExists,
    'tsconfig.json',
  );
  const readConfigFileResult = ts.readConfigFile(configPath, ts.sys.readFile);
  if (readConfigFileResult.error) {
    throw new Error(
      ts.formatDiagnostic(readConfigFileResult.error, formatHost),
    );
  }
  const jsonConfig = readConfigFileResult.config;
  const convertResult = ts.convertCompilerOptionsFromJson(
    jsonConfig.compilerOptions,
    './',
  );
  if (convertResult.errors && convertResult.errors.length > 0) {
    throw new Error(ts.formatDiagnostics(convertResult.errors, formatHost));
  }
  const compilerOptions = convertResult.options;
  return compilerOptions;
}

function displayFilename(originalFunc, operationName) {
  let displayEnabled = false;
  function displayFunction() {
    const fileName = arguments[0];
    if (displayEnabled) {
      process.stdout.clearLine();
      process.stdout.cursorTo(0);
      process.stdout.write(`${operationName}: ${fileName}`);
    }
    return originalFunc(...arguments);
  }
  displayFunction.originalFunc = originalFunc;
  displayFunction.enableDisplay = () => {
    if (process.stdout.isTTY) {
      displayEnabled = true;
    }
  };
  displayFunction.disableDisplay = () => {
    if (displayEnabled) {
      displayEnabled = false;
      process.stdout.clearLine();
      process.stdout.cursorTo(0);
    }
  };
  return displayFunction;
}

function compile() {
  const compilerOptions = getTSConfig();
  const compilerHost = ts.createCompilerHost(compilerOptions);
  compilerHost.readFile = displayFilename(compilerHost.readFile, 'Reading');
  compilerHost.readFile.enableDisplay();
  const program = ts.createProgram(
    ['./src/main.ts'],
    compilerOptions,
    compilerHost,
  );
  compilerHost.readFile.disableDisplay();

  console.log(
    ts.formatDiagnosticsWithColorAndContext(
      ts.getPreEmitDiagnostics(program),
      formatHost,
    ),
  );

  compilerHost.writeFile = displayFilename(compilerHost.writeFile, 'Emitting');
  compilerHost.writeFile.enableDisplay()
  const emitResult = program.emit();
  compilerHost.writeFile.disableDisplay();
  console.log(
    ts.formatDiagnosticsWithColorAndContext(emitResult.diagnostics, formatHost),
  );
  return emitResult.diagnostics.length === 0;
}

compile() && require('./dist/main');


Incremental program watcher своими руками


В предыдущем разделе мы посмотрели, как можно использовать TypeScript Compiler API для того, чтобы настроить процесс компиляции Typescript. Теперь пришла пора сделать нечто более насущное и полезное, что реально может повысить скорость и удобство разработки. В процессе компиляции программы компилятору необходимо разрешить все зависимости в программе.

Чтобы это сделать он последовательно считывает и анализирует файлы проекта и файлы библиотек. Даже для компиляции относительно небольшого проекта как у меня на этапе подготовки компилятору нужно считать, распарсить и проанализировать более 2000 файлов. Это приводит к значительному замедлению процесса компиляции, например у меня на ноутбуке компиляция занимает приблизительно 20 секунд. При этом в процессе разработки обычно между запусками компилятора меняются всего несколько файлов, поэтому логично, что начиная с версии 2.7 в Typescript появилась возможность инкрементальной компиляции, которая совместно с watch-режимом, позволяющим следить за изменениями файлов на диске, значительно повысила скорость повторной компиляции.

Эти функции использует и CLI Nest.js, а начать работу в этому режиме можно при помощи команды

nest start --watch


Другая возможность использовать преимущества такого режима разработки — запускать tsc c ключом --watch. Но, как я уже писал во вступлении, если возможностей стандартных команд в какой-то момент перестает хватать, самое время заглянуть под капот Typescript и узнать, как там оно работает.

Для начала, как и в предыдущем случае, создадим ts-wather.js со следующим содержимым:

const ts = require('typescript');

const formatHost = {
  getCanonicalFileName: path => path,
  getCurrentDirectory: ts.sys.getCurrentDirectory,
  getNewLine: () => ts.sys.newLine,
};

async function watchMain() {
  const configPath = ts.findConfigFile(
    './',
    ts.sys.fileExists,
    'tsconfig.json',
  );

  const host = ts.createWatchCompilerHost(
    configPath,
    {},
    ts.sys,
    ts.createEmitAndSemanticDiagnosticsBuilderProgram,
    diagnostic =>
      console.log(
        ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost),
      ),
    diagnostic =>
      console.log(
        'Watch status: ',
        ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost),
      ),
  );
  ts.createWatchProgram(host);
}
watchMain();


Запустим скрипт и посмотрим, как всё работает:

fojbqpahmqhjvjcscrwh_vj4etu.gif

После запуска компилятор отслеживает и перекомпилирует измененные файлы, выдает красиво оформленные сообщения об ошибках, в общем работает очень похоже на tsc с опцией --watch. Обратите внимание, как быстро происходит повторная компиляция! Но какой же профит нам от этого, спросите вы, если наш скрипт делает все тоже самое что tsc или nest start --watch, не проще ли использовать готовый инструмент? А профит наш в том, что мы теперь можем с помощью хуков делать разные штуки, которые изрядно облегчат нам жизнь.

Для того, чтобы нам было удобно хукать разные штуки в хосте, давайте напишем небольшой хелпер:

function on(host, functionName, before, after) {
  const originalFunction = host[functionName];
  host[functionName] = function() {
    before && before(...arguments);
    const result = originalFunction && originalFunction(...arguments);
    after && after(result);
    return result;
  };
}


Он позволит выполнять нужные нам действия до и после вызываемой функции в хосте, при этом получать аргументы, которые ей передают и результат выполнения. Теперь этот хэлпер можно использовать, чтобы удобно вмешиваться в работу компилятора на разных этапах. Для примера давайте добавим сообщения в консоль на этапе подготовки к компиляции:

…
    on(
    host,
    'createProgram',
    () => {
      console.log("** We're about to create the program! **");
    },
    () => {
      console.log('** Program created **');
    },
  );
…


Теперь посмотрим, что поменялось:

bfap-pqwd8buh9utntddvkwm9p8.gif

Работает! А теперь давайте добавим строку с отображением читаемых файлов. Для этого воспользуемся функцией displayFilename из предыдущего примера:

  host.readFile = displayFilename(host.readFile, 'Reading');

  on(
    host,
    'createProgram',
    () => {
      console.log("** We're about to create the program! **");
      host.readFile.enableDisplay();
    },
    () => {
      host.readFile.disableDisplay();
      console.log('** Program created **');
    },
  );


Запустим и посмотрим, что получилось:

1brjwcm13btajai1evwcsubuiu8.gif

Хорошо! Теперь сделаем то, ради чего все затевалось — перезапуск скомпилированной программы. Для этого надо сделать хук, который будет срабатывать после окончания записи на диск скомпилированных файлов.

let currentProgram;
  on(
    host,
    'afterProgramCreate',
    (program) => {
      console.log('** We finished making the program! Emitting... **');
      currentProgram = program;
    },
    () => {
      console.log('** Emit complete! **');
      const onAppClosed = () => {
        if (app) {
          setTimeout(onAppClosed, 100);
        } else {
          clearCache();
          require('./dist/bootstrap')
            .bootstrap()
            .then((res) => {
              app = res;
            });
        }
      };
      if (currentProgram && currentProgram.getSemanticDiagnostics().length === 0) {
        onAppClosed();
      }
    },
  );


Давайте разбираться, что здесь происходит. Во-первых — переменная app, в которую мы запишем результат выполнения функции bootstrap нам понадобится для того, чтобы потом, после изменения кода и начала повторной компиляции корректно закрыть сервер. В ней будет содержаться хэндлер для вызова команды остановки сервера. Во-вторых — запуск сервера происходит только в том случае, если в процессе компиляции не найдены ошибки в коде. Для этого мы проверяем при помощи getSemanticDiagnostics чтобы количество диагностик равнялось нулю. Ну и в-третьих — поскольку модульная система кэширует загруженные модули, после внесения изменений в код нам надо кэш почистить от зависимостей. Для этого я написал небольшую вспомогательную функцию clearCache, которую мы предварительно вызываем.

function clearCache() {
  const cacheKeys = Object.keys(require.cache);
  const paths = [
    join(__dirname, 'dist'),
    dirname(require.resolve('typeorm')),
    dirname(require.resolve('@nestjs/typeorm')),
  ];
  cacheKeys
    .filter((item) => paths.filter((p) => item.startsWith(p)).length > 0)
    .forEach((item, index, arr) => {
      delete require.cache[item];
      );
    });
}


Здесь производится очистка кэша от зависимостей, которые могут нам помешать корректно перезапустить сервер. В нашем случае из кэша надо удалить все модули нашего проекта из каталога dist. Также я удаляю модули typeorm, поскольку особенности их реализации мешают серверу перезапуститься (почему так происходит, это уже другая история). При помощи таймаута дожидаемся момента, когда сервер будет закрыт и переменная app будет очищена. Закрытие сервера мы будем производить в момент, когда компилятор обнаруживает изменения файлов и приступает к созданию программы с измененными файлами. Для этого дополним хук на функции createProgram.

…
  let app;
  on(
    host,
    'createProgram',
    () => {
      console.log("** We're about to create the program! **");
      app && app.close().then(() => (app = undefined));
      host.readFile.enableDisplay();
    },
    () => {
      host.readFile.disableDisplay();
      console.log('** Program created **');
    },
  );
…


Как видите — функция закрытия сервера — асинхронная, именно поэтому нам приходится городить огород с таймаутом.

Последний штрих — это подмена process.exit ().

process.exit = (code) => {
  console.log('!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!');
  console.trace(`Process try to exit with code ${code}.`);
};


Вообще так делать не стоит, но Nest.js имеет дурную привычку ее вызывать в случае, если в процессе запуска что-то пошло не так, поэтому мы просто заменим функцию выхода из программы на сообщение об ошибке. В этом случае наш скрипт продолжит работать после вызова process.exit (). Это надо сделать перед вызовом функции watchMain.

Итак, теперь запустим наш скрипт и посмотрим, что получилось:

oxbqwsea1k9ytcqmczaqfs6ass8.gif

Тут я вношу ошибку в код сервера, после этого сервер не запускается и выводятся сообщения об ошибках, после того как ошибка исправлена, сервер вновь успешно запускается. Ниже привожу получившийся в итоге скрипт:

ts-compiler.js
const ts = require('typescript');
const { join, dirname } = require('path');
const { exit } = require('process');

const formatHost = {
  getCanonicalFileName: (path) => path,
  getCurrentDirectory: ts.sys.getCurrentDirectory,
  getNewLine: () => ts.sys.newLine,
};

function on(host, functionName, before, after) {
  const originalFunction = host[functionName];
  host[functionName] = function () {
    before && before(...arguments);
    const result = originalFunction && originalFunction(...arguments);
    after && after(result);
    return result;
  };
}

function clearCache() {
  const cacheKeys = Object.keys(require.cache);
  const paths = [
    join(__dirname, 'dist'),
    dirname(require.resolve('typeorm')),
    dirname(require.resolve('@nestjs/typeorm')),
  ];
  cacheKeys
    .filter((item) => paths.filter((p) => item.startsWith(p)).length > 0)
    .forEach((item, index, arr) => {
      delete require.cache[item];
      process.stdout.clearLine(); // clear current text
      process.stdout.cursorTo(0); // move cursor to beginning of line
      process.stdout.write(
        `Clearing cache ${Math.floor((index * 100) / arr.length + 1)}%`,
      );
    });
  process.stdout.write(' finished.\n');
}

function displayFilename(originalFunc, operationName) {
  let displayEnabled = false;
  let counter = 0;
  function displayFunction() {
    const fileName = arguments[0];
    if (displayEnabled) {
      process.stdout.clearLine();
      process.stdout.cursorTo(0);
      process.stdout.write(`${operationName}: ${fileName}`);
    }
    counter++;
    return originalFunc(...arguments);
  }
  displayFunction.originalFunc = originalFunc;
  displayFunction.enableDisplay = () => {
    counter = 0;
    if (process.stdout.isTTY) {
      displayEnabled = true;
    }
  };
  displayFunction.disableDisplay = () => {
    if (displayEnabled) {
      displayEnabled = false;
      process.stdout.clearLine();
      process.stdout.cursorTo(0);
    }
    console.log(`${counter} times function was called`);
  };
  return displayFunction;
}

async function watchMain() {
  const configPath = ts.findConfigFile(
    './',
    ts.sys.fileExists,
    'tsconfig.json',
  );

  const host = ts.createWatchCompilerHost(
    configPath,
    {},
    ts.sys,
    ts.createEmitAndSemanticDiagnosticsBuilderProgram,
    (diagnostic) =>
      console.log(
        ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost),
      ),
    (diagnostic) =>
      console.log(
        'Watch status: ',
        ts.formatDiagnosticsWithColorAndContext([diagnostic], formatHost),
      ),
  );

  host.readFile = displayFilename(host.readFile, 'Reading');
  let app;
  on(
    host,
    'createProgram',
    () => {
      console.log("** We're about to create the program! **");
      app && app.close().then(() => (app = undefined));
      host.readFile.enableDisplay();
    },
    () => {
      host.readFile.disableDisplay();
      console.log('** Program created **');
    },
  );
  let currentProgram;
  on(
    host,
    'afterProgramCreate',
    (program) => {
      console.log('** We finished making the program! Emitting... **');
      currentProgram = program;
    },
    () => {
      console.log('** Emit complete! **');
      const onAppClosed = () => {
        if (app) {
          setTimeout(onAppClosed, 100);
        } else {
          clearCache();
          require('./dist/bootstrap')
            .bootstrap()
            .then((res) => {
              app = res;
            });
        }
      };
      if (currentProgram && currentProgram.getSemanticDiagnostics().length === 0) {
        onAppClosed();
      }
    },
  );

  ts.createWatchProgram(host);
}
process.exit = (code) => {
  console.log('!!!!!!!!!!!!!!!!!!!!!!! WARNING!!!!!!!!!!!!!!!!!!!!!!!!!');
  console.trace(`Process try to exit with code ${code}.`);
};
watchMain();


Выводы


В этом туториале мы посмотрели, как можно использовать возможности Typescript Compiler API для того, чтобы гибко организовать процесс отладки и сборки кода. Как видите, разработчики Typescript дают нам достаточно мощный и удобный в использовании API, чтобы не было необходимости привлекать для этих целей сторонние библиотеки и при этом заметно повысить удобство и скорость разработки. Надеюсь, что приведенная здесь информация будет вам полезна, спасибо, что дочитали до конца!

© Habrahabr.ru