Skip to content

Instantly share code, notes, and snippets.

@Leko
Last active October 20, 2019 08:30
Show Gist options
  • Save Leko/777eade9efc2c078a89b02350f752eaf to your computer and use it in GitHub Desktop.
Save Leko/777eade9efc2c078a89b02350f752eaf to your computer and use it in GitHub Desktop.
#deno_ja LT

deno内部のCompiler APIと非同期のお話

TL;DR
懲りもせずDenoコードリーディング話です
結果: https://twitter.com/L_e_k_o/status/1180148912922824704?s=20

About

Leko (https://twitter.com/L_e_k_o)

まえおき

DenoはTypeScriptのコードを実行する際に、内部的にJavaScript(AMD形式)にtranspile+bundleしてから実行しています。
細かい内部構造の話は割愛して、トランスパイルするところの周辺の処理(TypeScript)だけの話をする

cli/js/compiler.ts

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();

ex. importの解決

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)をリクエストされたときに逐次実行したら良いのでは・・・?

Compiler APIのAPIは同期

現状非同期は扱えないよう。
ファイルなら同期でも取得できるが、URLを同期で取るのは難しい

Make the Compiler and Language Service API Asynchronous · Issue #1857 · microsoft/TypeScript

つまり

CompilerHostが同期的にしか使えないため、事前に非同期処理となるHTTP通信をしておいて、メモリにファイルの内容を貯めてからコンパイルするようにしていた

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment