Как проверить тип фрагмента кода TypeScript в памяти?
Я реализую поддержку TypeScript в своем приложении Data-Forge Notebook.
Мне нужно скомпилировать, проверить тип и оценить фрагменты кода TypeScript.
Компиляция, кажется, не проблема, я использую transpileModule
как показано ниже, чтобы преобразовать фрагмент кода TS в код JavaScript, который можно оценить:
import { transpileModule, TranspileOptions } from "typescript";
const transpileOptions: TranspileOptions = {
compilerOptions: {},
reportDiagnostics: true,
};
const tsCodeSnippet = " /* TS code goes here */ ";
const jsOutput = transpileModule(tsCodeSnippet, transpileOptions);
console.log(JSON.stringify(jsOutput, null, 4));
Однако при попытке скомпилировать код TS с ошибкой возникает проблема.
Например, следующая функция имеет ошибку типа, но она передается без диагностики ошибок:
function foo(): string {
return 5;
}
Прозрачность - это здорово, но я также хотел бы, чтобы мой пользователь мог отображать ошибки.
Поэтому мой вопрос заключается в том, как это сделать, но также выполнять проверку типов и создавать ошибки для семантических ошибок?
Обратите внимание, что я не хочу сохранять код TypeScript в файл. Это было бы ненужным бременем производительности для моего приложения. Я только хочу скомпилировать и проверить тип фрагментов кода, которые хранятся в памяти.
3 ответа
Я решил эту проблему, опираясь на оригинальную помощь Дэвида Шеррета, а затем совет Фабиана Пиркльбауэра ( создателя TypeScript Playground).
Я создал прокси-сервер CompilerHost, чтобы обернуть настоящий CompilerHost. Прокси-сервер способен возвращать код TypeScript в памяти для компиляции. Базовый реальный CompilerHost способен загружать библиотеки TypeScript по умолчанию. Библиотеки необходимы, иначе вы получите множество ошибок, связанных со встроенными типами данных TypeScript.
Код
import * as ts from "typescript";
//
// A snippet of TypeScript code that has a semantic/type error in it.
//
const code
= "function foo(input: number) {\n"
+ " console.log('Hello!');\n"
+ "};\n"
+ "foo('x');"
;
//
// Result of compiling TypeScript code.
//
export interface CompilationResult {
code?: string;
diagnostics: ts.Diagnostic[]
};
//
// Check and compile in-memory TypeScript code for errors.
//
function compileTypeScriptCode(code: string, libs: string[]): CompilationResult {
const options = ts.getDefaultCompilerOptions();
const realHost = ts.createCompilerHost(options, true);
const dummyFilePath = "/in-memory-file.ts";
const dummySourceFile = ts.createSourceFile(dummyFilePath, code, ts.ScriptTarget.Latest);
let outputCode: string | undefined = undefined;
const host: ts.CompilerHost = {
fileExists: filePath => filePath === dummyFilePath || realHost.fileExists(filePath),
directoryExists: realHost.directoryExists && realHost.directoryExists.bind(realHost),
getCurrentDirectory: realHost.getCurrentDirectory.bind(realHost),
getDirectories: realHost.getDirectories.bind(realHost),
getCanonicalFileName: fileName => realHost.getCanonicalFileName(fileName),
getNewLine: realHost.getNewLine.bind(realHost),
getDefaultLibFileName: realHost.getDefaultLibFileName.bind(realHost),
getSourceFile: (fileName, languageVersion, onError, shouldCreateNewSourceFile) => fileName === dummyFilePath
? dummySourceFile
: realHost.getSourceFile(fileName, languageVersion, onError, shouldCreateNewSourceFile),
readFile: filePath => filePath === dummyFilePath
? code
: realHost.readFile(filePath),
useCaseSensitiveFileNames: () => realHost.useCaseSensitiveFileNames(),
writeFile: (fileName, data) => outputCode = data,
};
const rootNames = libs.map(lib => require.resolve(`typescript/lib/lib.${lib}.d.ts`));
const program = ts.createProgram(rootNames.concat([dummyFilePath]), options, host);
const emitResult = program.emit();
const diagnostics = ts.getPreEmitDiagnostics(program);
return {
code: outputCode,
diagnostics: emitResult.diagnostics.concat(diagnostics)
};
}
console.log("==== Evaluating code ====");
console.log(code);
console.log();
const libs = [ 'es2015' ];
const result = compileTypeScriptCode(code, libs);
console.log("==== Output code ====");
console.log(result.code);
console.log();
console.log("==== Diagnostics ====");
for (const diagnostic of result.diagnostics) {
console.log(diagnostic.messageText);
}
console.log();
Выход
==== Evaluating code ====
function foo(input: number) {
console.log('Hello!');
};
foo('x');
=========================
Diagnosics:
Argument of type '"x"' is not assignable to parameter of type 'number'.
Ситуация 1 - Использование только памяти - Нет доступа к файловой системе (например, в Интернете)
Это не простая задача и может занять некоторое время. Возможно, есть более простой способ, но я еще не нашел.
- Реализовать
ts.CompilerHost
где методы какfileExists
,readFile
,directoryExists
,getDirectories()
и т. д. читать из памяти вместо фактической файловой системы. - Загрузите соответствующие файлы lib в свою файловую систему в памяти в зависимости от того, что вам нужно (например, lib.es6.d.ts или lib.dom.d.ts).
- Добавьте файл в памяти в файловую систему в памяти.
- Создать программу (используя
ts.createProgram
) и перейдите в свой обычайts.CompilerHost
, - Вызов
ts.getPreEmitDiagnostics(program)
чтобы получить диагностику.
Несовершенный пример
Вот короткий несовершенный пример, который не реализует должным образом файловую систему в памяти и не загружает файлы lib (поэтому будут глобальные диагностические ошибки... их можно игнорировать или вы можете вызывать определенные методы в program
Кроме как program.getGlobalDiagnostics()
, Обратите внимание на поведение ts.getPreEmitDiagnostics
здесь):
import * as ts from "typescript";
console.log(getDiagnosticsForText("const t: number = '';").map(d => d.messageText));
function getDiagnosticsForText(text: string) {
const dummyFilePath = "/file.ts";
const textAst = ts.createSourceFile(dummyFilePath, text, ts.ScriptTarget.Latest);
const options: ts.CompilerOptions = {};
const host: ts.CompilerHost = {
fileExists: filePath => filePath === dummyFilePath,
directoryExists: dirPath => dirPath === "/",
getCurrentDirectory: () => "/",
getDirectories: () => [],
getCanonicalFileName: fileName => fileName,
getNewLine: () => "\n",
getDefaultLibFileName: () => "",
getSourceFile: filePath => filePath === dummyFilePath ? textAst : undefined,
readFile: filePath => filePath === dummyFilePath ? text : undefined,
useCaseSensitiveFileNames: () => true,
writeFile: () => {}
};
const program = ts.createProgram({
options,
rootNames: [dummyFilePath],
host
});
return ts.getPreEmitDiagnostics(program);
}
Ситуация 2 - Доступ к файловой системе
Если у вас есть доступ к файловой системе, тогда это намного проще, и вы можете использовать функцию, аналогичную приведенной ниже:
import * as path from "path";
function getDiagnosticsForText(
rootDir: string,
text: string,
options?: ts.CompilerOptions,
cancellationToken?: ts.CancellationToken
) {
options = options || ts.getDefaultCompilerOptions();
const inMemoryFilePath = path.resolve(path.join(rootDir, "__dummy-file.ts"));
const textAst = ts.createSourceFile(inMemoryFilePath, text, options.target || ts.ScriptTarget.Latest);
const host = ts.createCompilerHost(options, true);
overrideIfInMemoryFile("getSourceFile", textAst);
overrideIfInMemoryFile("readFile", text);
overrideIfInMemoryFile("fileExists", true);
const program = ts.createProgram({
options,
rootNames: [inMemoryFilePath],
host
});
return ts.getPreEmitDiagnostics(program, textAst, cancellationToken);
function overrideIfInMemoryFile(methodName: keyof ts.CompilerHost, inMemoryValue: any) {
const originalMethod = host[methodName] as Function;
host[methodName] = (...args: unknown[]) => {
// resolve the path because typescript will normalize it
// to forward slashes on windows
const filePath = path.resolve(args[0] as string);
if (filePath === inMemoryFilePath)
return inMemoryValue;
return originalMethod.apply(host, args);
};
}
}
// example...
console.log(getDiagnosticsForText(
__dirname,
"import * as ts from 'typescript';\n const t: string = ts.createProgram;"
));
Делая это таким образом, компилятор будет искать rootDir
для node_modules
папку и используйте там наборы (их не нужно загружать в память другим способом).
Я хотел оценить строку, представляющую машинописный текст, и:
- иметь видимость ошибок, связанных с типом
- уметь использовать операторы как для исходных файлов, так и для
node_modules
зависимости - иметь возможность повторно использовать настройки машинописного текста (
tsconfig.json
и т. д.) любого текущего проекта
Я достиг этого, написав временный файл и запустив его с помощью утилиты сchild_process.spawn
Это требуетts-node
работать в текущей оболочке; возможно, вам придется сделать:
npm install --global ts-node
или
npm install --save-dev ts-node
Этот код использует ts-node для запуска любого фрагмента машинописного кода:
import path from 'node:path';
import childProcess from 'node:child_process';
import fs from 'node:fs/promises';
let getTypescriptResult = async (tsSourceCode, dirFp=__dirname) => {
// Create temporary file storing the typescript code to execute
let tsPath = path.join(dirFp, `${Math.random().toString(36).slice(2)}.ts`);
await fs.writeFile(tsPath, tsSourceCode);
try {
// Run the ts-node shell command using the temporary file
let output = [] as Buffer[];
let proc = childProcess.spawn('ts-node', [ tsPath ], { shell: true, cwd: process.cwd() });
proc.stdout.on('data', d => output.push(d));
proc.stderr.on('data', d => output.push(d));
return {
code: await new Promise(r => proc.on('close', r)),
output: Buffer.concat(output).toString().trim()
};
} finally { await fs.rm(tsPath); } // Remove temporary file
};
Теперь я могу запустить:
let result = await getTypescriptResult('const str: string = 123;');
console.log(result.output);
И через какое-то трехзначное число миллисекунд я вижу, чтоresult.output
представляет собой многострочную строку с этим значением:
/Users/..../index.ts:859
return new TSError(diagnosticText, diagnosticCodes, diagnostics);
^
TSError: ⨯ Unable to compile TypeScript:
6y4ln36ox8c.ts(2,7): error TS2322: Type 'number' is not assignable to type 'string'.
at createTSError (/Users/..../index.ts:859:12)
at reportTSError (/Users/..../index.ts:863:19)
at getOutput (/Users/..../index.ts:1077:36)
at Object.compile (/Users/..../index.ts:1433:41)
at Module.m._compile (/Users/..../index.ts:1617:30)
at Module._extensions..js (node:internal/modules/cjs/loader:1159:10)
at Object.require.extensions.<computed> [as .ts] (/Users/..../index.ts:1621:12)
at Module.load (node:internal/modules/cjs/loader:981:32)
at Function.Module._load (node:internal/modules/cjs/loader:827:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:77:12) {
diagnosticCodes: [ 2322 ]
}
Здесь должны появиться все соответствующие данные, хотя может потребоваться некоторый анализ!
Этот подход также поддерживаетimport
заявления:
let typescript = `
import dependency from '@namespace/dependency';
import anotherDependency from './src/source-file';
doStuffWithImports(dependency, anotherDependency);
`;
let result = await getTypescriptResult(typescript, __dirname);
console.log(result.output);
Обратите внимание: если вы определяете в отдельном файле, вы можете передать__dirname
в качестве второго параметра при его вызове, чтобы разрешение модуля работало относительно текущего файла - в противном случае оно будет работать относительно файла, определяющегоgetTypescriptResult
.