5.14 Transpile

Overview

Once the graph of dependencies has been constructed, and all the required modules have been fetched and parsed, it's possible that these modules contain a combination of JavaScript (JS) and TypeScript (TS) code. However, since the V8 engine, which Deno uses, can only process JavaScript code, the next step is to handle the modules appropriately. This involves processing the dependency graph and transforming any TypeScript code into JavaScript. This phase is known as "checking" or "transpilation."

It's worth noting that the Deno runtime's primary concern is quick startup, which is why the deno run command is designed to focus on transpilation. In other words, when you use deno run, the modules will be transpiled into JavaScript to ensure a faster execution. On the other hand, if you specifically want to perform type checking for explicit type information verification, you should utilize the deno check command. This command ensures that the TypeScript code is checked for correctness in terms of types and other related aspects.

In the upcoming sections, we will explore both of these processes—transpilation and type checking. The choice between "check" and "transpile" depends on the sub-command you use:

  • When using deno run, the SWC compiler will be employed to perform transpilation.

  • When utilizing deno check, the TSC (TypeScript Compiler) will be used to perform a thorough type check on the codebase.

By understanding these two distinct approaches, you'll be well-equipped to handle the necessary steps to ensure your Deno applications are both efficiently executed and free from type-related issues.

Functionality

In previous iterations of Deno, type checking used to be integrated into the 'deno run' command. However, in more recent versions, this feature has been completely phased out from the 'deno run' command. When you execute a program now, there is no mandatory requirement for type checking during the startup phase. This is particularly true if your code editor has inherent type-checking capabilities; in such instances, it's likely that your code has been composed accurately in terms of types.

In scenarios where the editor provides robust type-checking, the initial type-checking step can be omitted. The primary motivation behind this omission is to enhance the startup speed of Deno. This optimization is aimed at facilitating quicker execution of your programs. It's worth noting that the SWC compiler, which is leveraged by Deno, boasts an approximate eightfold improvement in speed when compared to the traditional TypeScript Compiler (TSC).

If you find yourself needing to perform type-checking, either because your editor doesn't support it or as an extra layer of protection, you have a couple of options to consider. One option is to make use of Microsoft's TypeScript (TSC) compiler, which is coded in JavaScript. It's worth noting that this compiler tends to run rather slowly in comparison to SWC.

So, when it comes down to it, you have a choice to make based on your preferences. You can opt for the TSC compiler if you need that type-checking, even though it might be slower, or you can go with SWC for potentially faster performance. The decision ultimately rests in your hands as the user.

Transpile

In the process of transpilation in Deno, two significant files are generated as outputs. Let's take the example of a file named "helloLog.ts":

  1. helloLog.ts.js: This file holds the code that has been transformed and converted. It takes the TypeScript code from "helloLog.ts" and compiles it into JavaScript. This JavaScript version can be executed directly by the Deno runtime. It's the result of the transpilation process that allows TypeScript code, which is a higher-level language for developers, to be translated into the lower-level JavaScript code that computers understand.

  2. helloLog.ts.meta: In this file, you'll find a hash value. This hash serves an essential purpose – it acts as a fingerprint for the "helloLog.ts" file. Think of it as a unique identifier that represents the content and structure of the original TypeScript file. This hash is used by Deno to determine whether any changes have occurred in the "helloLog.ts" file. When you make modifications to the TypeScript code, Deno can compare the new hash with the one stored in the meta file. If the hashes differ, Deno knows that the file has been altered and requires transpilation again.

The journey begins with the initial TypeScript code you write. It might include functions, classes, variables, and other programming constructs. This code, residing in the "helloLog.ts" file, serves as the foundation. Deno's transpilation process takes this TypeScript code and generates the corresponding JavaScript code in the ".js" file. This conversion ensures that your code can be executed seamlessly within the Deno environment.

The "helloLog.ts.meta" file, though seemingly small, plays a crucial role in maintaining the integrity and efficiency of the development process. Its hash value acts as a guardian, helping Deno decide when it's time to perform transpilation. When you alter the TypeScript code, remember that the hash in the meta file will change too, signaling to Deno that a transformation is needed before the updated code can be executed.

The original file is as follows:

// File helloLog.ts

function printNumber(input: number) {
  console.log(input);
}

function printString(input: string) {
  console.log(input);
}

printNumber("One");
printString("One");

The transpiled JS file is as follows:

// File helloLog.ts
function printNumber(input) {
    console.log(input);
}
function printString(input) {
    console.log(input);
}
printNumber("One");
printString("One");
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbImZpbGU6Ly8vVXNlcnMvbWF5YW5rYy9Xb3JrL3NvdXJjZS9kZW5vRXhhbXBsZXMvaGVsbG9Mb2cudHMiXSwic291cmNlc0NvbnRlbnQiOlsiLy8gRmlsZSBoZWxsb0xvZy50c1xuXG5mdW5jdGlvbiBwcmludE51bWJlcihpbnB1dDogbnVtYmVyKSB7XG4gIGNvbnNvbGUubG9nKGlucHV0KTtcbn1cblxuZnVuY3Rpb24gcHJpbnRTdHJpbmcoaW5wdXQ6IHN0cmluZykge1xuICBjb25zb2xlLmxvZyhpbnB1dCk7XG59XG5cbnByaW50TnVtYmVyKFwiT25lXCIpO1xucHJpbnRTdHJpbmcoXCJPbmVcIik7XG4iXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsbUJBQW1CO0FBRW5CLFNBQVMsWUFBWSxLQUFhO0lBQ2hDLFFBQVEsSUFBSTtBQUNkO0FBRUEsU0FBUyxZQUFZLEtBQWE7SUFDaEMsUUFBUSxJQUFJO0FBQ2Q7QUFFQSxZQUFZO0FBQ1osWUFBWSJ9

The generated .meta file contains metadata associated with the source file. It's a JSON file with the following data:

{"source_hash":"11056719879154141555","emit_hash":"14271523416398949797"}

The term "source_hash" refers to the CRC checksum of the source file. For every individual file, a unique source_hash is calculated, and this calculated hash is then compared with the hash that is stored. Whenever these two hashes match each other, it signifies that the source content remains unchanged. As a result, there is no need to go through the transpilation process, which is a step that converts code from one programming language to another.

fn get_source_hash(&self, source_text: &str) -> u64 {
    FastInsecureHasher::new()
      .write_str(source_text)
      .write_u64(self.emit_options_hash)
      .finish()
}

In the Deno environment, the task of transpilation is carried out by the lightning fast SWC compiler, which is built upon the Rust programming language. This innovative compiler offers a significant speed advantage, being approximately 8 times quicker compared to the conventional TSC method. This means that code transformation, a crucial process in preparing code for execution, is accomplished more rapidly and efficiently with SWC.

pub fn transpile(&self, options: &EmitOptions) -> Result<TranspiledSource> {
    let program = (*self.program()).clone();
    let source_map = Rc::new(SourceMap::default());
    let source_map_config = SourceMapConfig {
      inline_sources: options.inline_sources,
    };
    let file_name = match ModuleSpecifier::parse(self.specifier()) {
      Ok(specifier) => FileName::Url(specifier),
      Err(_) => FileName::Custom(self.specifier().to_string()),
    };
    source_map.new_source_file(file_name, self.text_info().text().to_string());
    // needs to align with what's done internally in source map
    assert_eq!(1, self.text_info().range().start.as_byte_pos().0);
    // we need the comments to be mutable, so make it single threaded
    let comments = self.comments().as_single_threaded();
    let globals = Globals::new();
    crate::swc::common::GLOBALS.set(&globals, || {
      let top_level_mark = Mark::fresh(Mark::root());
      let program = fold_program(
        program,
        options,
        source_map.clone(),
        &comments,
        top_level_mark,
        self.diagnostics(),
      )?;

      let mut src_map_buf = vec![];
      let mut buf = vec![];
      {
        let mut writer = Box::new(JsWriter::new(
          source_map.clone(),
          "\n",
          &mut buf,
          Some(&mut src_map_buf),
        ));
        writer.set_indent_str("  "); // two spaces
        let config = crate::swc::codegen::Config {
          minify: false,
          ascii_only: false,
          omit_last_semi: false,
          target: ES_VERSION,
        };
        let mut emitter = crate::swc::codegen::Emitter {
          cfg: config,
          comments: Some(&comments),
          cm: source_map.clone(),
          wr: writer,
        };
        program.emit_with(&mut emitter)?;
      }
      let mut src = String::from_utf8(buf)?;
      let mut map: Option<String> = None;
      {
        let mut buf = Vec::new();
        source_map
          .build_source_map_with_config(&src_map_buf, None, source_map_config)
          .to_writer(&mut buf)?;

        if options.inline_source_map {
          src.push_str("//# sourceMappingURL=data:application/json;base64,");
          base64::encode_config_buf(
            buf,
            base64::Config::new(base64::CharacterSet::Standard, true),
            &mut src,
          );
        } else {
          map = Some(String::from_utf8(buf)?);
        }
      }
      Ok(TranspiledSource {
        text: src,
        source_map: map,
      })
    })
  }

Once the transpilation process is complete, we end up with two important components at our disposal:

  • The JavaScript source code

  • The source hash

The JavaScript source code is stored within a file named <app-name>.js, while both the source hash and emit hash are stored in a separate <app-name>.js.meta file. These files are neatly organized in a specific directory within DENO_DIR.

To illustrate, let's consider the initial code file named helloLog.ts, which was originally situated at the path /Users/mayankc/Work/source/denoExamples/helloLog.ts.

Following the transpilation, the resulting files are arranged in the following location:

> ls /Users/mayankc/Library/Caches/deno/gen/file/Users/mayankc/Work/source/denoExamples/helloLog.ts.*
/Users/mayankc/Library/Caches/deno/gen/file/Users/mayankc/Work/source/denoExamples/helloLog.ts.js
/Users/mayankc/Library/Caches/deno/gen/file/Users/mayankc/Work/source/denoExamples/helloLog.ts.meta

The code to save the generated JS & meta file is as follows:

fn set_emit_code_result(
    &self,
    specifier: &ModuleSpecifier,
    source_hash: u64,
    code: &str,
  ) -> Result<(), AnyError> {
    let meta_filename = self
      .get_meta_filename(specifier)
      .ok_or_else(|| anyhow!("Could not get meta filename."))?;
    let emit_filename = self
      .get_emit_filename(specifier)
      .ok_or_else(|| anyhow!("Could not get emit filename."))?;

    // save the metadata
    let metadata = EmitMetadata {
      source_hash: source_hash.to_string(),
      emit_hash: compute_emit_hash(code.as_bytes(), self.cli_version),
    };
    self
      .disk_cache
      .set(&meta_filename, &serde_json::to_vec(&metadata)?)?;

    // save the emit source
    self.disk_cache.set(&emit_filename, code.as_bytes())?;

    Ok(())
  }

--

That covered the process of obtaining, transpiling, and caching the code. We are now prepared to move on to the subsequent phase, which involves initiating the loading of the code into the V8 engine.

Last updated