LearnNewsExamplesServices

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:

  1. 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.
  2. No Special Directives: Logic isn't handled by template-specific directives like n-if or n-for. You use standard JavaScript (if/else, map()) for all conditionals and loops, which is more powerful and familiar.
  3. 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
  1. Flattening: It recursively flattens any nested templates into a single string and a corresponding array of dynamic values.
  2. 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).
  3. Self-Closing Tag Conversion: Since parse5 does not handle self-closing custom element tags, a regular expression adds explicit closing tags (e.g., <MyComponent /> becomes <MyComponent></MyComponent>).
  4. Parsing: The sanitized HTML string is parsed into a standard AST using parse5.parseFragment().
  5. VDOM Conversion: The processor traverses the parse5 AST and converts it back into a Neo.mjs VDOM object. During this process, it re-inserts the original dynamic values from the values array, 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:

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.

  1. Parse Code: It uses acorn to parse the JavaScript file content into an AST.
  2. Find Templates: It traverses the AST to find all html tagged template expressions.
  3. Process Template: Each template is processed by templateBuildProcessor.mjs, which converts the HTML-like syntax into a serializable VDOM object.
  4. Convert to AST: The resulting VDOM object is converted back into a valid AST ObjectExpression node.
  5. Replace Node: The original TaggedTemplateExpression is replaced in the main AST with the new ObjectExpression.
  6. 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 the processFileContent function from the astTemplateProcessor for 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 jsonToAst function uses acorn.parseExpressionAt to 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 the astTemplateProcessor turns into a plain Identifier in 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 parse5 library 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.