Kalan's Blog

Kalan 頭像照片,在淡水拍攝,淺藍背景

四零二曜日電子報上線啦!訂閱訂起來

Software Engineer / Taiwanese / Life in Fukuoka
This blog supports RSS feed (all content), you can click RSS icon or setup through third-party service. If there are special styles such as code syntax in the technical article, it is still recommended to browse to the original website for the best experience.

Current Theme light

我會把一些不成文的筆記或是最近的生活雜感放在短筆記,如果有興趣的話可以來看看唷!

Please notice that currenly most of posts are translated by AI automatically and might contain lots of confusion. I'll gradually translate the post ASAP

In-depth understanding of Svelte (1) — Svelte Compilation Process

Understanding Svelte in Depth (1) - Svelte Compilation Process

Before reading this article, it is expected that the readers have experience with Svelte or other frontend frameworks and are interested in implementation principles.

If you haven't read "How Svelte Compiles (0) - What is an Abstract Syntax Tree?" yet, it is recommended to read it before diving into this article.

In this article, we aim to answer a few questions:

  • Why can Svelte compile code into JavaScript?
  • Why can Svelte use template engine-like syntax ({#if} {#await}, etc.), and how is it different from regular template engines?

Introduction

To generate the final code, Svelte needs to compile the components once to obtain the necessary information. The compilation process in Svelte consists of several stages:

  • Parsing JavaScript, HTML + Svelte template syntax, and CSS syntax into an Abstract Syntax Tree (AST)
    • Parsing JavaScript (enclosed in <script> tags or within {}) using acorn to generate the AST
    • Parsing HTML + Svelte template syntax ({#if}, {variable}, etc.) using a custom parser from Svelte's compiler to generate the AST
    • Parsing CSS syntax using csstree to generate the AST
  • Calling new Component(ast) to generate the Svelte component, which includes information such as instance, fragment, vars, etc. (src/compiler/compile/Component.ts)
  • Calling renderer.render to generate the JavaScript and CSS

The actual flow and processing are more complex than described above (including intro, outro control, event listeners, variable tracking, etc.). The entire process can be referred to using this diagram:

Svelte Compile flow

1. Parsing Source Code into AST

First, Svelte divides the components into three main parts: HTML (including Svelte syntax), CSS, and JavaScript, which are parsed using different parsers.

If you want to see how a Svelte component looks after being parsed, you can check it out on AST Explorer:

<script>
  let count = 0;
  count++;
</script>

<style>
  p {
    font-size: 14px;
  }
</style>

<p>count is {count}</p>

The generated syntax tree (right-hand side):

Screenshot_2021-02-07 AST explorer(3)

After parsing, three ASTs are generated: html, css, and instance. The instance refers to the JavaScript code enclosed in <script> tags.

2. Generating Svelte Components (Component)

At this stage, Svelte stores the necessary information from the AST in the Component class. This information includes the component's HTML (named fragment in Svelte), declared variables, and the instance's AST.

Next, the instance (the part enclosed by <script> tags in the image above) is traversed to determine the usage of all variables. At this point, it is possible to detect variables that are declared but not used and variables with $ as a prefix that need to be handled.

Then, the HTML part is traversed, and a fragment is created. This part can be considered one of the core logics in the Svelte compilation. Fragments can be of various types, including regular HTML tags and Svelte syntax such as if and await.

// https://github.com/sveltejs/svelte/blob/master/src/compiler/compile/nodes/shared/map_children.ts
function get_constructor(type) {
	switch (type) {
		case 'AwaitBlock': return AwaitBlock;
		case 'Body': return Body;
		case 'Comment': return Comment;
		case 'EachBlock': return EachBlock;
		case 'Element': return Element;
		case 'Head': return Head;
		case 'IfBlock': return IfBlock;
		case 'InlineComponent': return InlineComponent;
		case 'KeyBlock': return KeyBlock;
		case 'MustacheTag': return MustacheTag;
		case 'Options': return Options;
		case 'RawMustacheTag': return RawMustacheTag;
		case 'DebugTag': return DebugTag;
		case 'Slot': return Slot;
		case 'Text': return Text;
		case 'Title': return Title;
		case 'Window': return Window;
		default: throw new Error(`Not implemented: ${type}`);
	}
}

For each type of fragment, Svelte creates a corresponding class for easier handling. The implementation details of each class are not explained here. Let's take a few examples:

  • Element corresponds to regular HTML tags and handles event handlers, attribute checks, and a11y checks.
    • For example, when the tagName is a but href is not added, a warning is thrown (source code).
  • IfBlock handles the syntax of {#if} and {:else}.
  • EachBlock handles the syntax of {#each}.

Next, Svelte hashes the corresponding CSS to prevent naming conflicts and generates the CSS styles.

3. Creating Fragments and Blocks

Finally, we come to the stage of generating the code. The logic for generating the code is defined in src/compiler/render_dom/Renderer.ts (Svelte chooses the renderer based on whether it is SSR or DOM; here, we use DOM as an example).

First, a fragment is created, and the code generation content is defined using the render function in the Wrapper. For example, Text.ts handles the generation of text. The fragment continuously traverses the child nodes and calls the render function to generate the corresponding code, which is then placed in a block.

Next, a block is declared, which contains many code fragments (e.g., code to be generated when mounting or unmounting), which will be used to create the create_fragment function. This part can be considered the most core and complex part of Svelte. The entire implementation can be viewed in src/compiler/render_dom/index.ts.

The code generation part uses code-red, written by the author Rich Harris, to facilitate generation. The special feature of this library is that it can directly generate the corresponding AST nodes using syntax like var a = 1. For example, the variableA in the example will actually become a VariableDeclaration node. It can also combine template literals to conveniently generate code.

For more information on code generation, you can refer to the IT Ironman video - Generating Component Code:

https://youtu.be/lxd6vsmL7RY

For example, if I want to dynamically generate an add function, I can write it like this:

Finally, the print API is used to convert the syntax tree into code. Let's take a look at the implementation in Svelte (using EachBlock.ts as an example, as other code generation parts are relatively complex) and see how the code generation is written:

// Called during component creation to invoke each_block_else.c()
block.chunks.create.push(b`
  if (${each_block_else}) {
    ${each_block_else}.c();
  }
`);

if (this.renderer.options.hydratable) {
  block.chunks.claim.push(b`
    if (${each_block_else}) {
      ${each_block_else}.l(${parent_nodes});
    }
  `);
}

// Called during component mounting to invoke each_block_else.m()
block.chunks.mount.push(b`
  if (${each_block_else}) {
    ${each_block_else}.m(${initial_mount_node}, ${initial_anchor_node});
  }
`);

The b being called is one of the APIs of code-red. These are the code snippets that will be generated.

Why can Svelte compile code into JavaScript?

Returning to the question at the beginning of this article, because Svelte compiles and analyzes the code in advance, it can compile .svelte files into JavaScript.

Why can Svelte use template engine-like syntax, and how is it different from regular template engines?

Svelte has implemented a custom parser that can parse not only regular HTML but also syntax inside {}. Through the aforementioned compilation process, it generates the corresponding JavaScript code. The difference from regular template engine syntax is that Svelte's syntax is reactive, while regular template engine syntax is static HTML. For example, let's take ERB as an example:

<% unless content.empty? %>
  <div>
    <%= content.text %>
  </div>
<% end %>

This syntax is typically used to render HTML on the backend and return it. However, in Svelte's syntax:

{#if content}
  <div>
    {content.text}  
  </div>
{/if}

the UI will be updated when content is not empty.

Conclusion

This article attempted to explain the general flow of Svelte from compilation to code generation, including parsing the code into an AST, creating fragments and corresponding nodes, and finally generating code through the renderer. It didn't delve much into the details and implementation, which will be explored in future articles. We hope that after reading this article, readers will have a better understanding of the process of code generation in Svelte.

References

Prev

An in-depth understanding of Svelte (0) — What is an Abstract Syntax Tree?

Next

4 Reasons to Learn Svelte in 2022

If you found this article helpful, please consider buy me a drink ☕️ It'll make my ordinary day shine✨

Buy me a coffee