Topic 1: The Module Pattern & ES6 Architecture
LegoDOM has evolved from a monolithic IIFE to a modern ES6 modular architecture. The codebase is now organized in src/ with clear separation of concerns.
The New Structure
src/
├── index.js # The Assembler - constructs the Lego global object
├── core/ # Core framework functionality
│ ├── batcher.js # Render queue management
│ ├── globals.js # Global state ($route, $go, $db)
│ ├── lifecycle.js # snap(), unsnap() - component lifecycle
│ ├── parser.js # defineLegoFile() - runtime SFB parser
│ ├── reactive.js # Proxy-based reactivity
│ ├── registry.js # Internal storage (registry, privateData, forPools)
│ ├── renderer.js # render(), bind() - DOM manipulation
│ └── stylesheets.js # Scoped CSS management
├── directives/ # Extracted directive implementations
│ ├── b-for.js
│ ├── b-if.js
│ ├── b-show.js
│ ├── b-sync.js
│ ├── b-text.js
│ ├── b-html.js
│ └── index.js # Directive registry
├── features/ # Optional features
│ ├── error.js # Error boundaries
│ ├── persistence.js # $db, localStorage management
│ └── router.js # $go, $route
└── utils/ # Shared utilities
├── dom.js # DOM helpers (escapeHTML, findAncestor, syncModelValue)
├── helpers.js # Core utilities (resolve, createRegex, deriveBlockName)
├── lru-cache.js # Expression cache
├── parser-utils.js # parseLego() - shared SFB parser
└── safe-eval.js # Sandboxed evaluationWhy Modular ES6?
The IIFE pattern served well initially, but as the codebase grew past 1500 lines, maintainability became an issue. The modular structure offers:
- Separation of Concerns: Each module has a single, well-defined responsibility.
- Testability: Individual modules can be tested in isolation.
- Tree-Shaking: Build tools can eliminate unused code.
- Developer Experience: Easier to navigate and understand.
- Circular Dependency Resolution:
src/index.jsacts as a dependency injection container.
The Assembler Pattern
src/index.js is the entry point. It doesn't contain business logic-it imports pieces and wires them together:
import { snap, unsnap } from './core/lifecycle.js';
import { render, bind } from './core/renderer.js';
import { globalBatcher } from './core/batcher.js';
import { setErrorHandlers } from './features/error.js';
// Dependency Injection
globalBatcher.setHandler(render);
setErrorHandlers(snap, render);
// Construct the Lego object
const Lego = {
snap,
unsnap,
block: (tagName, templateHTML, logic) => {
// Register block...
},
init: async (root) => {
// Initialize framework...
},
// ... other methods
};
export { Lego };
export default Lego;This pattern solves circular dependencies: lifecycle.js needs renderer.js, and renderer.js needs lifecycle.js. By injecting dependencies at runtime, we break the cycle.
The use of WeakMap
You'll notice the use of WeakMap in src/core/registry.js for privateData and forPools.
Why not a regular Map? A
WeakMapallows the keys (which are DOM elements) to be garbage collected if the element is removed from the DOM.Memory Leak Prevention: If we used a regular
Map, LegoDOM would hold a reference to every block ever created, even after deletion, eventually crashing the browser tab.
Internal Registry
Located in src/core/registry.js:
export const registry = {}; // tagName → <template> element
export const legoFileLogic = new Map(); // tagName → { mounted(), data, ... }
export const sharedStates = new Map(); // tagName → reactive singleton
export const privateData = new WeakMap(); // element → { snapped, bindings, ... }The registry acts as LegoDOM's "brain." It stores the <template> elements that define what a block looks like. When you write <my-button>, Lego looks into this object to find the blueprint.