В разработке приложений на 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;
}
Если теперь внести в наши исходники ошибку, получим в консоли красиво отформатированное сообщение:
Итак, мы получили скрипт, который будет компилировать наши исходники и отображать в консоли ошибки в случае их возникновения. Как видите, такой подход дает возможность очень гибко реализовать процесс компиляции, поскольку у нас есть доступ ко всем этапам и результатам компиляции. Дополнительной гибкости можно достичь, кастомизировав 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() возвращает истину, если после окончания работы не обнаружено ошибок. Теперь в случае, если компиляция прошла без ошибок можно сразу запустить скомпилированный сервер. Посмотрим, что получилось:
Вот полный код получившегося скрипта:
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();
Запустим скрипт и посмотрим, как всё работает:
После запуска компилятор отслеживает и перекомпилирует измененные файлы, выдает красиво оформленные сообщения об ошибках, в общем работает очень похоже на 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 **');
},
);
…
Теперь посмотрим, что поменялось:
Работает! А теперь давайте добавим строку с отображением читаемых файлов. Для этого воспользуемся функцией 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 **');
},
);
Запустим и посмотрим, что получилось:
Хорошо! Теперь сделаем то, ради чего все затевалось – перезапуск скомпилированной программы. Для этого надо сделать хук, который будет срабатывать после окончания записи на диск скомпилированных файлов.
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.
Итак, теперь запустим наш скрипт и посмотрим, что получилось:
Тут я вношу ошибку в код сервера, после этого сервер не запускается и выводятся сообщения об ошибках, после того как ошибка исправлена, сервер вновь успешно запускается. Ниже привожу получившийся в итоге скрипт:
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, чтобы не было необходимости привлекать для этих целей сторонние библиотеки и при этом заметно повысить удобство и скорость разработки. Надеюсь, что приведенная здесь информация будет вам полезна, спасибо, что дочитали до конца!
Автор: Михаил Соколов