TL;DR
懲りもせずDenoコードリーディング話です
結果: https://twitter.com/L_e_k_o/status/1180148912922824704?s=20
Leko (https://twitter.com/L_e_k_o)
DenoはTypeScriptのコードを実行する際に、内部的にJavaScript(AMD形式)にtranspile+bundleしてから実行しています。
細かい内部構造の話は割愛して、トランスパイルするところの周辺の処理(TypeScript)だけの話をする
class Host implements ts.CompilerHost {
CompilerHost
インタフェースを実装すると、TypeScriptのトランスパイラを作れる
DenoもTypeScriptをコンパイルするためのあれこれが実装されてる
// ts.createProgramの引数にHostのインスタンスを与える
const host = new Host(bundle);
const program = ts.createProgram(rootNames, options, host);
// 型チェック
ts.getPreEmitDiagnostics(program)
// JavaScript生成
const emitResult = program.emit();
Hostの中身の実装を見てみる。SourceFile.get
ってなんだろう?
private _getAsset(filename: string): SourceFile {
const sourceFile = SourceFile.get(filename); // <------- これ
if (sourceFile) {
return sourceFile;
}
const url = filename.split("/").pop()!;
const assetName = url.includes(".") ? url : `${url}.d.ts`;
const sourceCode = fetchAsset(assetName);
return new SourceFile({
url,
filename,
mediaType: MediaType.TypeScript,
sourceCode
});
}
getSourceFile(
fileName: string,
languageVersion: ts.ScriptTarget,
onError?: (message: string) => void,
shouldCreateNewSourceFile?: boolean
): ts.SourceFile | undefined {
util.log("compiler::host.getSourceFile", fileName);
try {
assert(!shouldCreateNewSourceFile);
const sourceFile = fileName.startsWith(ASSETS)
? this._getAsset(fileName)
: SourceFile.get(fileName); // <------- これ
assert(sourceFile != null);
if (!sourceFile!.tsSourceFile) {
sourceFile!.tsSourceFile = ts.createSourceFile(
fileName,
sourceFile!.sourceCode,
languageVersion
);
}
return sourceFile!.tsSourceFile;
} catch (e) {
if (onError) {
onError(String(e));
} else {
throw e;
}
return undefined;
}
}
SourceFile.get
はただメモリにファイルの中身を取ってきて保持する処理らしい
/** Recursively process the imports of modules, generating `SourceFile`s of any
* imported files.
*
* Specifiers are supplied in an array of tupples where the first is the
* specifier that will be requested in the code and the second is the specifier
* that should be actually resolved. */
async function processImports(
specifiers: Array<[string, string]>,
referrer = ""
): Promise<void> {
if (!specifiers.length) {
return;
}
const sources = specifiers.map(([, moduleSpecifier]) => moduleSpecifier);
const sourceFiles = await fetchSourceFiles(sources, referrer);
assert(sourceFiles.length === specifiers.length);
for (let i = 0; i < sourceFiles.length; i++) {
const sourceFileJson = sourceFiles[i];
const sourceFile =
SourceFile.get(sourceFileJson.url) || new SourceFile(sourceFileJson);
sourceFile.cache(specifiers[i][0], referrer);
if (!sourceFile.processed) {
await processImports(sourceFile.imports(), sourceFile.url);
}
}
}
// provide the "main" function that will be called by the privileged side when
// lazy instantiating the compiler web worker
window.compilerMain = function compilerMain(): void {
// workerMain should have already been called since a compiler is a worker.
window.onmessage = async ({ data }: { data: CompilerReq }): Promise<void> => {
const { rootNames, configPath, config, bundle } = data;
util.log(">>> compile start", { rootNames, bundle });
// This will recursively analyse all the code for other imports, requesting
// those from the privileged side, populating the in memory cache which
// will be used by the host, before resolving.
await processImports(rootNames.map(rootName => [rootName, rootName]));
このprocessImports
ってなぜ実行されてるんだろう?
Hostクラスでファイル(URL)をリクエストされたときに逐次実行したら良いのでは・・・?
現状非同期は扱えないよう。
ファイルなら同期でも取得できるが、URLを同期で取るのは難しい
— Make the Compiler and Language Service API Asynchronous · Issue #1857 · microsoft/TypeScript
CompilerHostが同期的にしか使えないため、事前に非同期処理となるHTTP通信をしておいて、メモリにファイルの内容を貯めてからコンパイルするようにしていた