5.14 Check and/or transpile

Overview

Once the graph is ready, all the modules have been fetched and parsed. The modules could still have a mix of JS and TS code. As v8 can only process JS code, it's time to process the graph and convert all the TS code into JS. This process is called check or transpile.
Check or transpile is chosen based on one of the arguments with the run command: --no--check.
  • If --no--check is specified in args
    • Do a transpile using the SWC compiler
  • Else
    • Do a check using the TSC compiler

Functionality

At the time of running the program, the user can choose not to run type-checking. If the editor supports type-checking, then possibly the code has been written correctly. In these cases, the type-checking at the startup can be skipped. The reason to do is to make startup faster. SWC compiler is around 8 times faster than TSC.
A very popular editor VSCode performs type-checking as the program is written. In this case, there may not be a need to run an additional type-check at startup. SWC compiler would quickly convert all the TS files to JS.
However, if type-checking is required, either unsupported by the editor or as another safeguard, Microsoft's TSC compiler would be used. This compiler is written in JS and runs quite slow when compared to SWC.
Either way is good, the choice is with the user.
Let's see the code where the decision is made (some code has been omitted):
// Program State
pub async fn prepare_module_load(
self: &Arc<Self>,
specifier: ModuleSpecifier,
lib: TypeLib,
runtime_permissions: Permissions,
is_dynamic: bool,
maybe_import_map: Option<ImportMap>,
) -> Result<(), AnyError> {
// --- CODE OMITTED --
let handler =
Rc::new(RefCell::new(FetchHandler::new(self, runtime_permissions)?));
let mut builder =
GraphBuilder::new(handler, maybe_import_map, self.lockfile.clone());
builder.add(&specifier, is_dynamic).await?;
let mut graph = builder.get_graph();
let debug = self.flags.log_level == Some(log::Level::Debug);
let maybe_config_path = self.flags.config_path.clone();
let result_modules = if self.flags.no_check {
let result_info = graph.transpile(TranspileOptions {
debug,
maybe_config_path,
reload: self.flags.reload,
})?;
// --- CODE OMITTED
} else {
let result_info = graph.check(CheckOptions {
debug,
emit: true,
lib,
maybe_config_path,
reload: self.flags.reload,
})?;
// --- CODE OMITTED
};
// --- CODE OMITTED
Ok(())
}

Output of check or transpile

Whether check or transpile, there are a series of files that get created. Here is the list of files:
  • helloLog.ts.js
  • helloLog.ts.meta
The JS file contains converted code. The .meta file contains metadata associated with the source file. It's a JSON file with the following data:
{"version_hash":"15897c2a6bcf0a1f72ad5a3a151927742db7e4b10a2d1a4f600f67b0f9d6e803"}
version_hash is the CRC checksum of the source file. The version_hash is calculated for each file and compared with the stored hash. If the hashes match, then the source is the same and the transpilation step can be skipped.
There are two main inputs for the calculation of version_hash:
  • source
  • Deno version
crate::checksum::gen(&[source.as_bytes(), version.as_bytes(), config])

Check

If --no-check is not supplied, there would be a round of type-checking at startup. This would be slower, but this is another safeguard against type errors. It might be better for the application startup to fail than throwing exceptions at runtime.
The check happens in two steps:
  • Type-check
  • TS to JS conversion
pub fn check(self, options: CheckOptions) -> Result<ResultInfo, AnyError> {
let mut config = TsConfig::new(json!({
"allowJs": true,
"esModuleInterop": true,
// Enabled by default to align to transpile/swc defaults
"experimentalDecorators": true,
"incremental": true,
"isolatedModules": true,
"lib": options.lib,
"module": "esnext",
"strict": true,
"target": "esnext",
"tsBuildInfoFile": "deno:///.tsbuildinfo",
}));
if options.emit {
config.merge(&json!({
"emitDecoratorMetadata": false,
"jsx": "react",
"inlineSourceMap": true,
"outDir": "deno://",
"removeComments": true,
}));
} else {
config.merge(&json!({
"noEmit": true,
}));
}
let maybe_ignored_options =
config.merge_tsconfig(options.maybe_config_path)?;
if !self.needs_emit(&config)
|| (self.is_emit_valid(&config)
&& (!options.reload || self.roots_dynamic))
{
debug!("graph does not need to be checked or emitted.");
return Ok(ResultInfo {
maybe_ignored_options,
loadable_modules: self.get_loadable_modules(),
..Default::default()
});
}
for specifier in &self.roots {
info!("{} {}", colors::green("Check"), specifier);
}
let root_names = self.get_root_names(!config.get_check_js());
let maybe_tsbuildinfo = self.maybe_tsbuildinfo.clone();
let hash_data =
vec![config.as_bytes(), version::deno().as_bytes().to_owned()];
let graph = Rc::new(RefCell::new(self));
let response = tsc::exec(
js::compiler_isolate_init(),
tsc::Request {
config: config.clone(),
debug: options.debug,
graph: graph.clone(),
hash_data,
maybe_tsbuildinfo,
root_names,
},
)?;
let mut graph = graph.borrow_mut();
graph.maybe_tsbuildinfo = response.maybe_tsbuildinfo;
// Only process changes to the graph if there are no diagnostics and there
// were files emitted.
if response.diagnostics.is_empty() {
if !response.emitted_files.is_empty() {
let mut codes = HashMap::new();
let mut maps = HashMap::new();
let check_js = config.get_check_js();
for emit in &response.emitted_files {
if let Some(specifiers) = &emit.maybe_specifiers {
assert!(specifiers.len() == 1, "Unexpected specifier length");
let specifier = graph.resolve_specifier(&specifiers[0]);
if !check_js
&& graph.get_media_type(&specifier) == Some(MediaType::JavaScript)
{
debug!("skipping emit for {}", specifier);
continue;
}
match emit.media_type {
MediaType::JavaScript => {
codes.insert(specifier.clone(), emit.data.clone());
}
MediaType::SourceMap => {
maps.insert(specifier.clone(), emit.data.clone());
}
_ => unreachable!(),
}
}
}
let config = config.as_bytes();
for (specifier, code) in codes.iter() {
if let ModuleSlot::Module(module) =
graph.get_module_mut(specifier).unwrap()
{
module.set_emit(code.clone(), maps.get(specifier).cloned());
module.set_version(&config);
module.is_dirty = true;
} else {
return Err(GraphError::MissingSpecifier(specifier.clone()).into());
}
}
}
graph.flush()?;
}
Ok(ResultInfo {
diagnostics: response.diagnostics,
loadable_modules: graph.get_loadable_modules(),
maybe_ignored_options,
stats: response.stats,
})
}
check() is a pretty big function. The check runs in a number of steps. Let's see them one by one.

Step 1 - Prepare TSC config

First, the TSC config is prepared. These are the TSC compiler options. There are some default options, and there are options that can come from the build info file. Some of the default options are:
  • use strict
  • check JS
  • remove comments
  • esnext
  • allow JS code
  • etc.

Step 2 - Check if a check is required

The next step is to check if a check is even required. If there is no change in either the module or its dependencies, then there is no need to run a type-check every time. If the code hasn't changed, then the startup can get faster as there would be no need to check or convert. In short, if reload is not required and the version hash is the same, do not check.
There is a check for the version hash of the module against the stored hash. If the hash hasn't changed, a check won't be required.
pub fn is_emit_valid(&self, config: &[u8]) -> bool {
if let Some(version) = self.maybe_version.clone() {
version == get_version(&self.source, &version::deno(), config)
} else {
false
}
}
If a check is required, the famous check line will be seen on the console:
A check line on the console is an indicator that a type-check would be performed followed by conversion to JS code.
The check line on the console comes only for type-checking. It doesn't come for transpilation.

Step 3 - Check and convert

A request would be submitted to the TSC compiler for type-checking and conversion. An important point to note is that check and convert request is submitted only for the root names.
let response = tsc::exec(
js::compiler_isolate_init(),
tsc::Request {
config: config.clone(),
debug: options.debug,
graph: graph.clone(),
hash_data,
maybe_tsbuildinfo,
root_names,
},
)?;

Step 4 - Go through emitted files

A check on a graph could result in emitted files. This generally happens when the main module has imports, which would be the normal case, except for simple test programs like our hello world. We'll go through the idea behind emitted files in the next chapter when we'll go over an example involving imports.
Once the check function is done, all the modules have been type-checked and converted to javascript. The modules are ready to load into v8 followed by instantiation.

Transpile

The other option is to go for transpile directly. As mentioned above, the user can decide to skip type-check for two reasons:
  • IDE has already performed type-check
  • A faster startup is desirable over slow type-checking
Transpile is done by Rust based SWC compiler. SWC is around 8 times faster than TSC.
pub fn transpile(
&mut self,
options: TranspileOptions,
) -> Result<ResultInfo, AnyError> {
let start = Instant::now();
let mut ts_config = TsConfig::new(json!({
"checkJs": false,
"emitDecoratorMetadata": false,
"inlineSourceMap": true,
"jsx": "react",
"jsxFactory": "React.createElement",
"jsxFragmentFactory": "React.Fragment",
}));
let maybe_ignored_options =
ts_config.merge_tsconfig(options.maybe_config_path)?;
let emit_options: ast::EmitOptions = ts_config.clone().into();
let mut emit_count: u128 = 0;
let config = ts_config.as_bytes();
for (_, module_slot) in self.modules.iter_mut() {
if let ModuleSlot::Module(module) = module_slot {
// if the module is a Dts file we should skip it
if module.media_type == MediaType::Dts {
continue;
}
// if we don't have check_js enabled, we won't touch non TypeScript or JSX
// modules
if !(emit_options.check_js
|| module.media_type == MediaType::JSX
|| module.media_type == MediaType::TSX
|| module.media_type == MediaType::TypeScript)
{
continue;
}
// skip modules that already have a valid emit
if !options.reload && module.is_emit_valid(&config) {
continue;
}
if module.maybe_parsed_module.is_none() {
module.parse()?;
}
let parsed_module = module.maybe_parsed_module.clone().unwrap();
let emit = parsed_module.transpile(&emit_options)?;
emit_count += 1;
module.maybe_emit = Some(Emit::Cli(emit));
module.set_version(&config);
module.is_dirty = true;
}
}
self.flush()?;
let stats = Stats(vec![
("Files".to_string(), self.modules.len() as u128),
("Emitted".to_string(), emit_count),
("Total time".to_string(), start.elapsed().as_millis()),
]);
Ok(ResultInfo {
diagnostics: Default::default(),
loadable_modules: self.get_loadable_modules(),
maybe_ignored_options,
stats,
})
}
}
The process of transiplation is quite similar to check. The only major difference being that transpile uses SWC. Let's go over the steps briefly.

Step 1 - Prepare config

Like check, the first step here is to prepare config for the SWC compiler. Here are some of the default options:
  • Do not check JS
  • Do not emit decorator metadata
  • Use react options

Step 2 - Check if transpile is required

This is exactly same as the step 2 of check. If reload is not required and the version hash is valid, do not transpile.

Step 3 - Parse module

Parse module if it hasn't been parsed. Module parsing is also done when the module is visited in the graph.

Step 4 - Transpile module

Transpile the module. This is where conversion from TS to JS happens. The following is an abbreviated code of the place where SWC transpiles the code:
{
let writer = Box::new(JsWriter::new(
self.source_map.clone(),
"\n",
&mut buf,
Some(&mut src_map_buf),
));
let config = swc_ecmascript::codegen::Config { minify: false };
let mut emitter = swc_ecmascript::codegen::Emitter {
cfg: config,
comments: Some(&self.comments),
cm: self.source_map.clone(),
wr: writer,
};
program.emit_with(&mut emitter)?;
}
Unlike the famous check line on the console, --no-check option doesn't print anything.