Under the Hood: The Philosophy and Mechanics of HTML Templates
This guide explores the "why" and "how" behind the HTML template feature in Neo.mjs. It's a deep dive into an architecture designed to deliver on a core framework promise: a zero-builds, instant-feedback development mode that doesn't sacrifice production performance.
This serves as a companion to the Using HTML Templates guide, which focuses on syntax and best practices.
The Core Philosophy: Why Not JSX?
One of the most critical design goals for Neo.mjs is to provide a zero-builds development environment. We believe that developers should be able to write code and see their changes instantly in the browser, without a mandatory compilation step. This philosophy directly informed our approach to UI templating.
Frameworks like React and Angular rely on non-standard syntax (JSX, Angular templates) that must be compiled into valid JavaScript. This requirement for a build step, even in development, introduces complexity and slows down the feedback loop.
Neo.mjs chose a different path: Tagged Template Literals. This is a standard, native JavaScript feature. By using
html... ``, we are not inventing a new language; we are leveraging the power of JavaScript itself. This allows for:
- True Zero-Builds Development: Your template code runs directly in the browser. What you write is what you get, with no hidden magic or required transformations.
- No Special Directives: Logic isn't handled by template-specific directives like
n-iforn-for. You use standard JavaScript (if/else,map()) for all conditionals and loops, which is more powerful and familiar. - Architectural Purity: The template is just a function call that returns a data structure (a VDOM object). This maintains a clean separation between your view definition and the framework's rendering engine.
Mechanism 1: The Zero-Builds Development Experience
In development mode, templates must be parsed at runtime. This is where the trade-off for instant feedback becomes apparent.
Conditional Loading: A Smart Optimization
To parse HTML strings, we need an HTML parser. Neo.mjs uses parse5, a robust and spec-compliant library. However, at
~176KB, we don't want to load it unless absolutely necessary.
This is why the parser is only loaded if a component on the page actually uses an HTML template. This check happens
inside the initAsync method of Neo.functional.component.Base.
NEO_CODE_BLOCK_0
If enableHtmlTemplates is true, the component dynamically imports the HtmlTemplateProcessor, which in turn pulls in
parse5. This ensures that applications not using this feature pay no performance penalty.
The Runtime Parsing Process
When a component's createVdom() method returns an HtmlTemplate object, it's handed off to the HtmlTemplateProcessor.
You can inspect its source code here:
src/functional/util/HtmlTemplateProcessor.mjs.
The processor executes a series of steps to convert the template literal into a VDOM object, which are detailed in the expandable section below.
Detailed Runtime Parsing Steps
- Flattening: It recursively flattens any nested templates into a single string and a corresponding array of dynamic values.
- Placeholder Injection: It replaces dynamic values (e.g., event handlers, component configs, other components)
with special placeholders in the string (e.g.,
__DYNAMIC_VALUE_0__,neotag1). - Self-Closing Tag Conversion: Since
parse5does not handle self-closing custom element tags, a regular expression adds explicit closing tags (e.g.,<MyComponent />becomes<MyComponent></MyComponent>). - Parsing: The sanitized HTML string is parsed into a standard AST using
parse5.parseFragment(). - VDOM Conversion: The processor traverses the
parse5AST and converts it back into a Neo.mjs VDOM object. During this process, it re-inserts the original dynamic values from thevaluesarray, preserving their rich data types (functions, objects, etc.). It also carefully reconstructs the original case-sensitive tag names for custom components.
Once the VDOM is constructed, it's passed back to the component's continueUpdateWithVdom() method, and the standard
rendering lifecycle proceeds.
Mechanism 2: Maximum Performance for Production
For production, the goal is to achieve the exact same VDOM output as the development mode, but with zero runtime parsing overhead. This is accomplished with a powerful build-time AST (Abstract Syntax Tree) transformation.
This work is handled by two main scripts:
- buildScripts/util/templateBuildProcessor.mjs: Contains the core logic for parsing the template string and converting it to a serializable VDOM object.
- buildScripts/util/astTemplateProcessor.mjs:
Orchestrates the overall process of reading a JS file, finding
htmltemplates, and replacing them with the final VDOM object via AST manipulation.
The AST Transformation Process
The astTemplateProcessor.mjs script is a marvel of build-time engineering. Instead of just doing a simple text
replacement, it performs a full AST transformation to ensure 100% accuracy.
- Parse Code: It uses
acornto parse the JavaScript file content into an AST. - Find Templates: It traverses the AST to find all
htmltagged template expressions. - Process Template: Each template is processed by
templateBuildProcessor.mjs, which converts the HTML-like syntax into a serializable VDOM object. - Convert to AST: The resulting VDOM object is converted back into a valid AST
ObjectExpressionnode. - Replace Node: The original
TaggedTemplateExpressionis replaced in the main AST with the newObjectExpression. - Generate Code: The modified AST is converted back into a string of JavaScript code using
astring.
As a developer convenience, if a template is the return value of a method named render, the build script automatically
renames the method to createVdom.
Integration with Build Environments
This logic is seamlessly integrated into all three of Neo.mjs's production build environments:
dist/esm: The buildScripts/buildESModules.mjs script directly invokes theprocessFileContentfunction from theastTemplateProcessorfor each JavaScript file before minification.dist/dev&dist/prod: These environments use Webpack. The transformation is handled by a custom loader: buildScripts/webpack/loader/template-loader.mjs. This loader is strategically applied only to the App worker's build configuration, an optimization that saves build time by not processing code for other workers.
Key Differences from Runtime Parsing
The build-time process is fundamentally different from the runtime parsing:
- No Lexical Scope: The build script cannot access runtime variables like
this. It captures the raw code (e.g.,this.name) as a string. - Placeholder Wrapping: These code strings are wrapped in special placeholders (e.g.,
##__NEO_EXPR__this.name##__NEO_EXPR__##). - Custom Resolution: During the VDOM-to-AST conversion, the
jsonToAstfunction usesacorn.parseExpressionAtto parse these placeholders back into proper AST nodes, perfectly preserving the original expressions for runtime evaluation. - Component Tag Handling: A tag like
<MyComponent>is converted into a placeholder object ({ __neo_component_name__: 'MyComponent' }) which theastTemplateProcessorturns into a plainIdentifierin the final AST.
Conclusion: The Neo.mjs Advantage
The dual-mode approach to HTML templates is a perfect example of the Neo.mjs philosophy in action. It provides:
- An Unmatched Developer Experience: The zero-builds development mode offers an instant feedback loop that is impossible in frameworks requiring mandatory compilation. You write standard JavaScript and it simply works.
- Maximum Production Performance: The build-time AST transformation ensures that your production code is as fast
as possible, with no client-side parsing overhead. The
parse5library is completely eliminated from your final bundle. - Architectural Consistency: The system is designed to produce the exact same VDOM structure in both modes. This eliminates a whole class of bugs where development and production environments behave differently.
This architecture isn't just a feature; it's a statement. It demonstrates a commitment to web standards, developer productivity, and end-user performance that sets Neo.mjs apart from the crowd.