Skip to content

Commit 0b65a2d

Browse files
committed
Create "Under the Hood: HTML Templates" Guide #7163
1 parent dcb46a5 commit 0b65a2d

1 file changed

Lines changed: 156 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
# Under the Hood: The Philosophy and Mechanics of HTML Templates
2+
3+
This guide explores the "why" and "how" behind the HTML template feature in Neo.mjs. It's a deep dive into an architecture
4+
designed to deliver on a core framework promise: a zero-builds, instant-feedback development mode that doesn't sacrifice
5+
production performance.
6+
7+
This serves as a companion to the [Using HTML Templates](./HtmlTemplates.md) guide, which focuses on syntax
8+
and best practices.
9+
10+
## The Core Philosophy: Why Not JSX?
11+
12+
One of the most critical design goals for Neo.mjs is to provide a **zero-builds development environment**. We believe
13+
that developers should be able to write code and see their changes instantly in the browser, without a mandatory
14+
compilation step. This philosophy directly informed our approach to UI templating.
15+
16+
Frameworks like React and Angular rely on non-standard syntax (JSX, Angular templates) that **must** be compiled into
17+
valid JavaScript. This requirement for a build step, even in development, introduces complexity and slows down
18+
the feedback loop.
19+
20+
Neo.mjs chose a different path: **Tagged Template Literals**. This is a standard, native JavaScript feature. By using
21+
`html`... ``, we are not inventing a new language; we are leveraging the power of JavaScript itself. This allows for:
22+
23+
1. **True Zero-Builds Development:** Your template code runs directly in the browser. What you write is what you get,
24+
with no hidden magic or required transformations.
25+
2. **No Special Directives:** Logic isn't handled by template-specific directives like `n-if` or `n-for`. You use
26+
standard JavaScript (`if/else`, `map()`) for all conditionals and loops, which is more powerful and familiar.
27+
3. **Architectural Purity:** The template is just a function call that returns a data structure (a VDOM object).
28+
This maintains a clean separation between your view definition and the framework's rendering engine.
29+
30+
## Mechanism 1: The Zero-Builds Development Experience
31+
32+
In development mode, templates must be parsed at runtime. This is where the trade-off for instant feedback becomes
33+
apparent.
34+
35+
### Conditional Loading: A Smart Optimization
36+
37+
To parse HTML strings, we need an HTML parser. Neo.mjs uses `parse5`, a robust and spec-compliant library. However, at
38+
~176KB, we don't want to load it unless absolutely necessary.
39+
40+
This is why the parser is **only loaded if a component on the page actually uses an HTML template**. This check happens
41+
inside the `initAsync` method of `Neo.functional.component.Base`.
42+
43+
```javascript
44+
// src/functional/component/Base.mjs
45+
async initAsync() {
46+
await super.initAsync();
47+
48+
if (this.enableHtmlTemplates && Neo.config.environment === 'development') {
49+
if (!Neo.ns('Neo.functional.util.HtmlTemplateProcessor')) {
50+
const module = await import('../util/HtmlTemplateProcessor.mjs');
51+
this.htmlTemplateProcessor = module.default
52+
}
53+
}
54+
}
55+
```
56+
57+
If `enableHtmlTemplates` is true, the component dynamically imports the `HtmlTemplateProcessor`, which in turn pulls in
58+
`parse5`. This ensures that applications not using this feature pay no performance penalty.
59+
60+
### The Runtime Parsing Process
61+
62+
When a component's `createVdom()` method returns an `HtmlTemplate` object, it's handed off to the `HtmlTemplateProcessor`.
63+
You can inspect its source code here:
64+
[src/functional/util/HtmlTemplateProcessor.mjs](../../../../src/functional/util/HtmlTemplateProcessor.mjs).
65+
66+
The processor executes a series of steps to convert the template literal into a VDOM object, which are detailed in the
67+
expandable section below.
68+
69+
<details>
70+
<summary>Detailed Runtime Parsing Steps</summary>
71+
72+
1. **Flattening:** It recursively flattens any nested templates into a single string and a corresponding array of
73+
dynamic values.
74+
2. **Placeholder Injection:** It replaces dynamic values (e.g., event handlers, component configs, other components)
75+
with special placeholders in the string (e.g., `__DYNAMIC_VALUE_0__`, `neotag1`).
76+
3. **Self-Closing Tag Conversion:** Since `parse5` does not handle self-closing custom element tags, a regular
77+
expression adds explicit closing tags (e.g., `<MyComponent />` becomes `<MyComponent></MyComponent>`).
78+
4. **Parsing:** The sanitized HTML string is parsed into a standard AST using `parse5.parseFragment()`.
79+
5. **VDOM Conversion:** The processor traverses the `parse5` AST and converts it back into a Neo.mjs VDOM object.
80+
During this process, it re-inserts the original dynamic values from the `values` array, preserving their rich data
81+
types (functions, objects, etc.). It also carefully reconstructs the original case-sensitive tag names for custom
82+
components.
83+
84+
</details>
85+
86+
Once the VDOM is constructed, it's passed back to the component's `continueUpdateWithVdom()` method, and the standard
87+
rendering lifecycle proceeds.
88+
89+
## Mechanism 2: Maximum Performance for Production
90+
91+
For production, the goal is to achieve the exact same VDOM output as the development mode, but with **zero runtime
92+
parsing overhead**. This is accomplished with a powerful build-time AST (Abstract Syntax Tree) transformation.
93+
94+
This work is handled by two main scripts:
95+
96+
- [buildScripts/util/templateBuildProcessor.mjs](../../../../buildScripts/util/templateBuildProcessor.mjs):
97+
Contains the core logic for parsing the template string and converting it to a serializable VDOM object.
98+
- [buildScripts/util/astTemplateProcessor.mjs](../../../../buildScripts/util/astTemplateProcessor.mjs):
99+
Orchestrates the overall process of reading a JS file, finding `html` templates, and replacing them with the final
100+
VDOM object via AST manipulation.
101+
102+
### The AST Transformation Process
103+
104+
The `astTemplateProcessor.mjs` script is a marvel of build-time engineering. Instead of just doing a simple text
105+
replacement, it performs a full AST transformation to ensure 100% accuracy.
106+
107+
1. **Parse Code:** It uses `acorn` to parse the JavaScript file content into an AST.
108+
2. **Find Templates:** It traverses the AST to find all `html` tagged template expressions.
109+
3. **Process Template:** Each template is processed by `templateBuildProcessor.mjs`, which converts the HTML-like syntax
110+
into a serializable VDOM object.
111+
4. **Convert to AST:** The resulting VDOM object is converted back into a valid AST `ObjectExpression` node.
112+
5. **Replace Node:** The original `TaggedTemplateExpression` is replaced in the main AST with the new `ObjectExpression`.
113+
6. **Generate Code:** The modified AST is converted back into a string of JavaScript code using `astring`.
114+
115+
As a developer convenience, if a template is the return value of a method named `render`, the build script automatically
116+
renames the method to `createVdom`.
117+
118+
### Integration with Build Environments
119+
120+
This logic is seamlessly integrated into all three of Neo.mjs's production build environments:
121+
122+
- **`dist/esm`:** The [buildScripts/buildESModules.mjs](../../../../buildScripts/buildESModules.mjs) script directly
123+
invokes the `processFileContent` function from the `astTemplateProcessor` for each JavaScript file before minification.
124+
- **`dist/dev` & `dist/prod`:** These environments use Webpack. The transformation is handled by a custom loader:
125+
[buildScripts/webpack/loader/template-loader.mjs](../../../../buildScripts/webpack/loader/template-loader.mjs).
126+
This loader is strategically applied **only to the App worker's build configuration**, an optimization that saves
127+
build time by not processing code for other workers.
128+
129+
### Key Differences from Runtime Parsing
130+
131+
The build-time process is fundamentally different from the runtime parsing:
132+
133+
- **No Lexical Scope:** The build script cannot access runtime variables like `this`. It captures the raw code (e.g.,
134+
`this.name`) as a string.
135+
- **Placeholder Wrapping:** These code strings are wrapped in special placeholders (e.g.,
136+
`##__NEO_EXPR__this.name##__NEO_EXPR__##`).
137+
- **Custom Resolution:** During the VDOM-to-AST conversion, the `jsonToAst` function uses `acorn.parseExpressionAt`
138+
to parse these placeholders back into proper AST nodes, perfectly preserving the original expressions for runtime
139+
evaluation.
140+
- **Component Tag Handling:** A tag like `<MyComponent>` is converted into a placeholder object
141+
(`{ __neo_component_name__: 'MyComponent' }`) which the `astTemplateProcessor` turns into a plain `Identifier` in
142+
the final AST.
143+
144+
## Conclusion: The Neo.mjs Advantage
145+
146+
The dual-mode approach to HTML templates is a perfect example of the Neo.mjs philosophy in action. It provides:
147+
148+
- **An Unmatched Developer Experience:** The zero-builds development mode offers an instant feedback loop that is
149+
impossible in frameworks requiring mandatory compilation. You write standard JavaScript and it simply works.
150+
- **Maximum Production Performance:** The build-time AST transformation ensures that your production code is as fast
151+
as possible, with no client-side parsing overhead. The `parse5` library is completely eliminated from your final bundle.
152+
- **Architectural Consistency:** The system is designed to produce the exact same VDOM structure in both modes.
153+
This eliminates a whole class of bugs where development and production environments behave differently.
154+
155+
This architecture isn't just a feature; it's a statement. It demonstrates a commitment to web standards, developer
156+
productivity, and end-user performance that sets Neo.mjs apart from the crowd.

0 commit comments

Comments
 (0)