Skip to content
This repository has been archived by the owner on May 19, 2023. It is now read-only.

Commit

Permalink
Merge pull request #128 from aidenybai/staging
Browse files Browse the repository at this point in the history
Staging
  • Loading branch information
aidenybai authored Apr 22, 2021
2 parents 29239d3 + ddcffcc commit f252efb
Show file tree
Hide file tree
Showing 32 changed files with 294 additions and 227 deletions.
33 changes: 0 additions & 33 deletions .github/CONTRIBUTING.md

This file was deleted.

4 changes: 1 addition & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,14 @@ package-lock.json
yarn-error.log

# Tests
test*.html
*.html
coverage

# Build source
dist/

# Editor Config
.vscode/
.idea/
settings.json
.DS_Store

# Misc
Expand Down
7 changes: 6 additions & 1 deletion .npmignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,13 @@ yarn.lock
yarn-error.log

# Testing
test*.html
*.html
coverage

# Development Distributions
*.map
*.dev.*

# Build Config
tsconfig.json
rollup.config.js
Expand All @@ -33,5 +37,6 @@ settings.json

# Misc
.github/
scripts/
docs/
scratch.md
8 changes: 8 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"recommendations": [
"esbenp.prettier-vscode",
"dbaeumer.vscode-eslint",
"aidenybai.lucia",
"ritwickdey.liveserver"
]
}
15 changes: 15 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
// Use the project's typescript version
"typescript.tsdk": "node_modules/typescript/lib",

// Use prettier to format typescript, javascript and JSON files
"[typescript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[javascript]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
},
"[json]": {
"editor.defaultFormatter": "esbenp.prettier-vscode"
}
}
105 changes: 9 additions & 96 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -1,100 +1,13 @@
# Core Documentation
## Lucia Documentation

This document covers how Lucia's core works. It's intended to aid in understanding the code, and helping contributors work with it.
**Overall Guidelines**
<br>_First time contributing to Lucia? Look no further! :)_

Note that there are some design decisions that make Lucia's core somewhat unorthodox. Keep in mind that this project is quite young and unstable, and some of the implementations are bound to change down the road.
- [`CONTRIBUTING.md`](guidelines/CONTRIBUTING.md) - Our standards for contributing, here on Github and anywhere else!
- [`CODE_OF_CONDUCT.md`](guidelines/CODE_OF_CONDUCT.md) - Our guidelines and conduct rules for contributors!

The Lucia's core isn't used to be rendered, mutated, and synced with the DOM, rather it is used as a reference of dynamic nodes during rendering. This mean in the core, no explicit diffing occurs and values that are compiled are readonly.
**Codebase Documentation**
<br>_Get started on running/developing Lucia._

The reasoning behind this architectural decision isn't necessarily because it is more efficient, rather it's just not necessary in Lucia's use case.

## Design Principles

The Lucia' core is designed to accomplish a balance between being **fast and compact**, by trying to execute as **few DOM operations** as it can. It achieves this by relying on directives created by the user.

- **Avoid doing unnecessary work**

Lucia only compiles the AST with only dynamic nodes, with static nodes being garbage collected. Lucia also optimizes the AST by making directives, dependencies, and the state's size immutable. This allows for straightforward dependency tracking and thereby making the least amount of DOM operations possible.

- **Balance mutability while enforcing simple patterns**

Many patterns in other libraries, such as the mutability of the view are often expensive on performance. Lucia attempts to resolve this through immutable directives (and thereby dependencies), allowing flexibility for the user while maintaining good performance. This way the runtime renderer does not need to check depedencies, interpretation, etc. every render cycle.

- **Keep the core as lightweight as possible**

The goal of Lucia is to be as light as possible, meaning that to achieve this, less code needs to be written. The core should be as fundemental and simple as possible, with abstractions filling in the additional functionality.

## Overview

<p align="center"><img src="https://raw.githubusercontent.com/aidenybai/lucia/master/.github/img/flowchart.svg" alt="Diagram of build pipeline" width="752"></p>

Lucia's Core is composed of two phases: compilation and runtime.

### Compiler

The compiler's purpose is to generate an AST for the renderer to reference. It first fetches all of the nodes under the specified node, inclusive of its root, then flattening it into an array. After that, it systematically picks out dynamic nodes through two conditions:

1. Has directives `(STATIC)`
2. Has dependencies in directives `(DYNAMIC)`
3. Static node `(NULL)`

Passing these two conditions will result in the creation of the AST.

**Abstract Syntax Tree**

The AST is an array of ASTNodes. An ASTNode looks like this:

```ts
interface ASTNode {
directives: {...};
deps: string[];
el: HTMLElement;
type: -1 | 0 | 1;
}
```

The `directives` property is used to data that includes reusuable functions of the directives on the specific element. We will talk more about this later. The `deps` property contains an array of dependency keys of all the directives of the element. The `el` property contains the element for the renderer to use. The `type` property can only be `0 (STATIC)` or `1 (DYNAMIC)`. This is important as the renderer garbage collects static nodes, which do not contain any dependencies.

**Directives and DirectiveData**

The values of the `directives` object are `DirectiveData`, which contain properties that the renderer can use. This is what it looks like:

```ts
interface DirectiveData {
compute: (state: UnknownKV, event?: Event) => any;
value: string;
deps: string[];
}
```

The `compute` function interprets and evaluates the `value`, passing the state from `compute`'s state parameter. Notice how there is a duplicate `deps` property for the `DirectiveData`. This functionally is the same as the ASTNode `deps`, but is for more fine tuned for dependency tracking. This pertains only to its own directive, while the ASTNode `deps` pertains to all of the directives.

**Performance Decisions**

The compiler intentionally handles a lot of the decision-making, such as dependency-tracking and only using dynamic nodes. These actions allow for better performace at runtime, but requires immutability. This makes Lucia less flexible, but it is possible to achieve the same goal with different patterns.

### Renderer

The renderer's purpose is to change the DOM based on the state. It does this by iterating over the AST from the compiler, checking dependencies against changed dependencies supplied by the observer, and rendering directives if necessary.

**Garbage Collection**

There are two types of ASTNodes as designated by the compiler: `0 (STATIC)` and `1 (DYNAMIC)`. Static ASTNodes refer to ASTNodes with directives, but no dependencies. Since directives are immutable, these nodes only need to be rendered once. After they are rendered, they are pushed to a queue. After all affected ASTNodes are rendered, they are deleted from the AST. This means that unnecessary iteration is removed, boosting performance.

**Expression Computation and Interpretation**

Since directives are special attributes, the value of directives are strings. Lucia first attempts to determine the exact dependency, so it can just access by state property. It currently supports direct key (`prop`), bypassing the need to evalute the expression. If it is not able to interpret the properties from the directive value, it will use the [`new Function()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) syntax to execute.

### Observer

The observer's purpose is to detect changes in the state and run a callback render function on change. This is useful because we only want to render if the state changes, as the content of the DOM is directly connected to the state.

To do this, a JavaScript object is provided, [sealed](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal), and wrapped with a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). This allows the `get` and `set` traps to be set. The observer automatically attempts to proxify nested arrays and objects, so that callback renders are able to be handled on change.

**Special Cases**

Some cases, such as array mutations using methods, such as `push` and `pop`, Proxy's are updated two times. The first change is a change in value, then in length. This can vary in order or in presense based on the type of mutation, meaning that both traps need to be accountd for. This means it renders both times, which is a minor performance bottleneck.

Another peculiarity of Proxy's is that changed information (`target`, `key`, `value`) are based on the current object, not the root object. This means that if there is a nested object in the state, the target will not be the root node, messing up our dependencies. What the observer currently does is go to root and attempt to find the affected object that contains the dependencies.

Lastly, methods are immutable. This is because there is dependency-tracking during compilation on the stringified content of methods.
- [`WORKFLOW.md`](codebase/WORKFLOW.md) - How to get started with iterating, building, and ad-hoc testing Lucia.
- [`CORE.md`](codebase/CORE.md) - Understanding the internals and how the core is structured.
100 changes: 100 additions & 0 deletions docs/codebase/CORE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# Core Documentation

This document covers how Lucia's core works. It's intended to aid in understanding the code, and helping contributors work with it.

Note that there are some design decisions that make Lucia's core somewhat unorthodox. Keep in mind that this project is quite young and unstable, and some of the implementations are bound to change down the road.

The Lucia's core isn't used to be rendered, mutated, and synced with the DOM, rather it is used as a reference of dynamic nodes during rendering. This mean in the core, no explicit diffing occurs and values that are compiled are readonly.

The reasoning behind this architectural decision isn't necessarily because it is more efficient, rather it's just not necessary in Lucia's use case.

## Design Principles

The Lucia' core is designed to accomplish a balance between being **fast and compact**, by trying to execute as **few DOM operations** as it can. It achieves this by relying on directives created by the user.

- **Avoid doing unnecessary work**

Lucia only compiles the AST with only dynamic nodes, with static nodes being garbage collected. Lucia also optimizes the AST by making directives, dependencies, and the state's size immutable. This allows for straightforward dependency tracking and thereby making the least amount of DOM operations possible.

- **Balance mutability while enforcing simple patterns**

Many patterns in other libraries, such as the mutability of the view are often expensive on performance. Lucia attempts to resolve this through immutable directives (and thereby dependencies), allowing flexibility for the user while maintaining good performance. This way the runtime renderer does not need to check depedencies, interpretation, etc. every render cycle.

- **Keep the core as lightweight as possible**

The goal of Lucia is to be as light as possible, meaning that to achieve this, less code needs to be written. The core should be as fundemental and simple as possible, with abstractions filling in the additional functionality.

## Overview

<p align="center"><img src="https://raw.githubusercontent.com/aidenybai/lucia/master/.github/img/flowchart.svg" alt="Diagram of build pipeline" width="752"></p>

Lucia's Core is composed of two phases: compilation and runtime.

### Compiler

The compiler's purpose is to generate an AST for the renderer to reference. It first fetches all of the nodes under the specified node, inclusive of its root, then flattening it into an array. After that, it systematically picks out dynamic nodes through two conditions:

1. Has directives `(STATIC)`
2. Has dependencies in directives `(DYNAMIC)`
3. Static node `(NULL)`

Passing these two conditions will result in the creation of the AST.

**Abstract Syntax Tree**

The AST is an array of ASTNodes. An ASTNode looks like this:

```ts
interface ASTNode {
directives: {...};
deps: string[];
el: HTMLElement;
type: -1 | 0 | 1;
}
```

The `directives` property is used to data that includes reusuable functions of the directives on the specific element. We will talk more about this later. The `deps` property contains an array of dependency keys of all the directives of the element. The `el` property contains the element for the renderer to use. The `type` property can only be `0 (STATIC)` or `1 (DYNAMIC)`. This is important as the renderer garbage collects static nodes, which do not contain any dependencies.

**Directives and DirectiveData**

The values of the `directives` object are `DirectiveData`, which contain properties that the renderer can use. This is what it looks like:

```ts
interface DirectiveData {
compute: (state: UnknownKV, event?: Event) => any;
value: string;
deps: string[];
}
```

The `compute` function interprets and evaluates the `value`, passing the state from `compute`'s state parameter. Notice how there is a duplicate `deps` property for the `DirectiveData`. This functionally is the same as the ASTNode `deps`, but is for more fine tuned for dependency tracking. This pertains only to its own directive, while the ASTNode `deps` pertains to all of the directives.

**Performance Decisions**

The compiler intentionally handles a lot of the decision-making, such as dependency-tracking and only using dynamic nodes. These actions allow for better performace at runtime, but requires immutability. This makes Lucia less flexible, but it is possible to achieve the same goal with different patterns.

### Renderer

The renderer's purpose is to change the DOM based on the state. It does this by iterating over the AST from the compiler, checking dependencies against changed dependencies supplied by the observer, and rendering directives if necessary.

**Garbage Collection**

There are two types of ASTNodes as designated by the compiler: `0 (STATIC)` and `1 (DYNAMIC)`. Static ASTNodes refer to ASTNodes with directives, but no dependencies. Since directives are immutable, these nodes only need to be rendered once. After they are rendered, they are pushed to a queue. After all affected ASTNodes are rendered, they are deleted from the AST. This means that unnecessary iteration is removed, boosting performance.

**Expression Computation and Interpretation**

Since directives are special attributes, the value of directives are strings. Lucia first attempts to determine the exact dependency, so it can just access by state property. It currently supports direct key (`prop`), bypassing the need to evalute the expression. If it is not able to interpret the properties from the directive value, it will use the [`new Function()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Function) syntax to execute.

### Observer

The observer's purpose is to detect changes in the state and run a callback render function on change. This is useful because we only want to render if the state changes, as the content of the DOM is directly connected to the state.

To do this, a JavaScript object is provided, [sealed](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/seal), and wrapped with a [Proxy](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy). This allows the `get` and `set` traps to be set. The observer automatically attempts to proxify nested arrays and objects, so that callback renders are able to be handled on change.

**Special Cases**

Some cases, such as array mutations using methods, such as `push` and `pop`, Proxy's are updated two times. The first change is a change in value, then in length. This can vary in order or in presense based on the type of mutation, meaning that both traps need to be accountd for. This means it renders both times, which is a minor performance bottleneck.

Another peculiarity of Proxy's is that changed information (`target`, `key`, `value`) are based on the current object, not the root object. This means that if there is a nested object in the state, the target will not be the root node, messing up our dependencies. What the observer currently does is go to root and attempt to find the affected object that contains the dependencies.

Lastly, methods are immutable. This is because there is dependency-tracking during compilation on the stringified content of methods.
31 changes: 31 additions & 0 deletions docs/codebase/WORKFLOW.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Workflow Documentation

Lucia is written in [TypeScript](https://www.typescriptlang.org) and should be run in a browser environment. We highly recommend you use [VSCode](https://code.visualstudio.com/) as your IDE when developing.

### Yarn Scripts

- `dev` - This script builds the codebase and watches for changes into a `iife` distribution bundle using [esbuild](http://esbuild.github.io/)
- `build` - This script builds the codebase into a `iife`, `cjs`, and `esm` format distribution bundles using [Rollup](https://rollupjs.org/)
- `lint` - This script uses [ESLint](https://eslint.org/) to lint the codebase
- `lint:fix` - This script uses [ESLint](https://eslint.org/) to lint the codebase and attempts to fix any errors
- `cleanup` - This script uses [Prettier](https://prettier.io/) to format the codebase
- `test` - This script runs unit tests (specified under `__test__` folders) using [Jest](https://jestjs.io/)
- `release` - This script runs the aformentioned scripts and publishes the project on NPM

### Iterating

You can create a `*.html` (e.g. `test.html`) file at root to test changes in realtime. We recommend using `live-server` to hot-reload the webpage on change, and edit as necessary.

Below is a sample for a Lucia starter:

```html
<!DOCTYPE html>
<html lang="en">
<head>
<script src="./dist/lucia.dev.js"></script>
</head>
<body>
<!-- Your code here -->
</body>
</html>
```
File renamed without changes.
File renamed without changes.
17 changes: 17 additions & 0 deletions docs/guidelines/CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# Contributing to Lucia

### Initial Steps:

1. Fork this repository and clone it to your local machine
2. Make sure you have `yarn` installed. If you don't, run `npm install -g yarn`
3. Install all packages with the `yarn` command in the project root.

Once you are setup, you check out the codebase documentation to learn more!
- [`WORKFLOW.md`](../codebase/WORKFLOW.md) - How to get started with iterating, building, and ad-hoc testing Lucia.
- [`CORE.md`](../codebase/CORE.md) - Understanding the internals and how the core is structured.

## Next Steps + Useful Info:

- We are using Yarn as our package manager, please do not commit your `package-lock.json` files from NPM
- Make sure you are upto date by doing `git pull` here and there.
- Submit a <a href="https://github.com/aidenybai/lucia/pulls">pull request</a>!
Loading

0 comments on commit f252efb

Please sign in to comment.