A library for constructing Web Components
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Server-side rendering support with hydration capabilities for fast initial page loads and SEO optimization, enabling isomorphic application development.
Utilities for generating and processing hydration markers that enable client-side hydration of server-rendered content.
/**
* Utilities for hydration markup generation and processing
*/
const HydrationMarkup: {
/** The attribute name used to mark hydratable elements */
readonly attributeMarkerName: string;
/**
* Creates a start marker for content binding hydration
* @param index - The binding index
* @param targetNodeId - The ID of the target node
* @returns HTML comment marker string
*/
contentBindingStartMarker(index: number, targetNodeId: string): string;
/**
* Creates an end marker for content binding hydration
* @param index - The binding index
* @returns HTML comment marker string
*/
contentBindingEndMarker(index: number): string;
/**
* Creates a marker for attribute binding hydration
* @param index - The binding index
* @param targetNodeId - The ID of the target node
* @param attributeName - The name of the attribute
* @returns HTML comment marker string
*/
attributeBindingMarker(index: number, targetNodeId: string, attributeName: string): string;
/**
* Parses hydration markers from HTML content
* @param html - The HTML content to parse
* @returns Parsed hydration information
*/
parseHydrationMarkers(html: string): HydrationInfo;
};
/**
* Hydration information extracted from markers
*/
interface HydrationInfo {
/** Content binding locations */
contentBindings: Array<{
index: number;
targetNodeId: string;
startNode: Comment;
endNode: Comment;
}>;
/** Attribute binding locations */
attributeBindings: Array<{
index: number;
targetNodeId: string;
attributeName: string;
marker: Comment;
}>;
/** Hydratable elements */
hydratableElements: Array<{
element: Element;
templateId: string;
}>;
}Usage Examples:
import { ViewTemplate, html } from "@microsoft/fast-element";
import { HydrationMarkup } from "@microsoft/fast-element/element-hydration.js";
// Template that supports hydration
const userCardTemplate = html<UserCard>`
<div class="user-card" ${HydrationMarkup.attributeMarker('data-user-id', x => x.userId)}>
${HydrationMarkup.contentBindingStartMarker(0, 'user-name')}
<h2 id="user-name">${x => x.name}</h2>
${HydrationMarkup.contentBindingEndMarker(0)}
${HydrationMarkup.contentBindingStartMarker(1, 'user-email')}
<p id="user-email">${x => x.email}</p>
${HydrationMarkup.contentBindingEndMarker(1)}
<div class="user-stats">
${HydrationMarkup.contentBindingStartMarker(2, 'post-count')}
<span id="post-count">Posts: ${x => x.postCount}</span>
${HydrationMarkup.contentBindingEndMarker(2)}
${HydrationMarkup.contentBindingStartMarker(3, 'follower-count')}
<span id="follower-count">Followers: ${x => x.followerCount}</span>
${HydrationMarkup.contentBindingEndMarker(3)}
</div>
</div>
`;
// Server-side rendering function
function renderUserCardSSR(userData: UserData): string {
// Server-side template rendering with hydration markers
let html = `
<div class="user-card" data-user-id="${userData.userId}" ${HydrationMarkup.attributeMarkerName}="user-card-template">
${HydrationMarkup.contentBindingStartMarker(0, 'user-name')}
<h2 id="user-name">${userData.name}</h2>
${HydrationMarkup.contentBindingEndMarker(0)}
${HydrationMarkup.contentBindingStartMarker(1, 'user-email')}
<p id="user-email">${userData.email}</p>
${HydrationMarkup.contentBindingEndMarker(1)}
<div class="user-stats">
${HydrationMarkup.contentBindingStartMarker(2, 'post-count')}
<span id="post-count">Posts: ${userData.postCount}</span>
${HydrationMarkup.contentBindingEndMarker(2)}
${HydrationMarkup.contentBindingStartMarker(3, 'follower-count')}
<span id="follower-count">Followers: ${userData.followerCount}</span>
${HydrationMarkup.contentBindingEndMarker(3)}
</div>
</div>
`;
return html;
}
// Client-side hydration process
class HydrationManager {
static hydrateUserCard(element: Element, userData: UserData): UserCard {
// Parse hydration info
const hydrationInfo = HydrationMarkup.parseHydrationMarkers(element.outerHTML);
// Create component instance
const userCard = new UserCard();
userCard.userId = userData.userId;
userCard.name = userData.name;
userCard.email = userData.email;
userCard.postCount = userData.postCount;
userCard.followerCount = userData.followerCount;
// Hydrate the element
this.hydrateElement(userCard, element, hydrationInfo);
return userCard;
}
private static hydrateElement(
component: any,
element: Element,
hydrationInfo: HydrationInfo
): void {
// Process content bindings
hydrationInfo.contentBindings.forEach(binding => {
const targetElement = element.querySelector(`#${binding.targetNodeId}`);
if (targetElement) {
// Establish reactive binding for content
this.establishContentBinding(component, targetElement, binding.index);
}
});
// Process attribute bindings
hydrationInfo.attributeBindings.forEach(binding => {
const targetElement = element.querySelector(`#${binding.targetNodeId}`);
if (targetElement) {
// Establish reactive binding for attribute
this.establishAttributeBinding(
component,
targetElement,
binding.attributeName,
binding.index
);
}
});
}
private static establishContentBinding(
component: any,
element: Element,
bindingIndex: number
): void {
// Set up reactive content binding
Observable.getNotifier(component).subscribe({
handleChange: () => {
// Update content based on component state
this.updateElementContent(component, element, bindingIndex);
}
});
}
private static establishAttributeBinding(
component: any,
element: Element,
attributeName: string,
bindingIndex: number
): void {
// Set up reactive attribute binding
Observable.getNotifier(component).subscribe({
handleChange: () => {
// Update attribute based on component state
this.updateElementAttribute(component, element, attributeName, bindingIndex);
}
});
}
private static updateElementContent(
component: any,
element: Element,
bindingIndex: number
): void {
// Update logic based on binding index and component state
switch (bindingIndex) {
case 0: // User name
element.textContent = component.name;
break;
case 1: // User email
element.textContent = component.email;
break;
case 2: // Post count
element.textContent = `Posts: ${component.postCount}`;
break;
case 3: // Follower count
element.textContent = `Followers: ${component.followerCount}`;
break;
}
}
private static updateElementAttribute(
component: any,
element: Element,
attributeName: string,
bindingIndex: number
): void {
// Update attribute based on component state
if (attributeName === 'data-user-id') {
element.setAttribute(attributeName, component.userId);
}
}
}
interface UserData {
userId: string;
name: string;
email: string;
postCount: number;
followerCount: number;
}
class UserCard {
userId: string = '';
name: string = '';
email: string = '';
postCount: number = 0;
followerCount: number = 0;
}Template interfaces and implementations that support server-side rendering and client-side hydration.
/**
* Checks if an object supports hydration
* @param obj - The object to check
* @returns True if the object is hydratable
*/
function isHydratable(obj: any): obj is { [Hydratable]: true };
/**
* Symbol that marks an object as hydratable
*/
const Hydratable: unique symbol;
/**
* Element view template that supports hydration
*/
interface HydratableElementViewTemplate<TSource = any, TParent = any>
extends ElementViewTemplate<TSource, TParent> {
/**
* Hydrates an element view from server-rendered content
* @param firstChild - First child node of the hydration range
* @param lastChild - Last child node of the hydration range
* @param hostBindingTarget - The element that host behaviors will be bound to
*/
hydrate(
firstChild: Node,
lastChild: Node,
hostBindingTarget?: Element
): ElementView<TSource, TParent>;
}
/**
* Synthetic view template that supports hydration
*/
interface HydratableSyntheticViewTemplate<TSource = any, TParent = any>
extends SyntheticViewTemplate<TSource, TParent> {
/**
* Hydrates a synthetic view from server-rendered content
* @param firstChild - First child node of the hydration range
* @param lastChild - Last child node of the hydration range
*/
hydrate(firstChild: Node, lastChild: Node): SyntheticView<TSource, TParent>;
}
/**
* View interface that supports hydration
*/
interface HydratableView extends HTMLView {
/** Indicates this view supports hydration */
readonly isHydratable: true;
/**
* Hydrates the view from existing DOM content
* @param firstChild - First child node to hydrate
* @param lastChild - Last child node to hydrate
*/
hydrate(firstChild: Node, lastChild: Node): void;
}Usage Examples:
import {
isHydratable,
Hydratable,
HydratableElementViewTemplate,
ViewTemplate,
html,
FASTElement,
customElement
} from "@microsoft/fast-element";
// Create hydratable template
function createHydratableTemplate<T>(): HydratableElementViewTemplate<T> {
const template = html<T>`
<div class="hydratable-content">
<h1>${x => (x as any).title}</h1>
<p>${x => (x as any).description}</p>
</div>
`;
// Mark template as hydratable
(template as any)[Hydratable] = true;
// Add hydration method
(template as any).hydrate = function(
firstChild: Node,
lastChild: Node,
hostBindingTarget?: Element
) {
// Create view from existing DOM
const view = this.create(hostBindingTarget);
// Hydrate from existing nodes
view.hydrate(firstChild, lastChild);
return view;
};
return template as HydratableElementViewTemplate<T>;
}
// Hydratable component
@customElement("hydratable-component")
export class HydratableComponent extends FASTElement {
title: string = "Default Title";
description: string = "Default Description";
// Use hydratable template
static template = createHydratableTemplate<HydratableComponent>();
// Support SSR hydration
static supportsHydration = true;
connectedCallback() {
super.connectedCallback();
// Check if this component needs hydration
if (this.needsHydration()) {
this.performHydration();
}
}
private needsHydration(): boolean {
// Check for hydration markers or server-rendered content
return this.hasAttribute('data-ssr') ||
this.querySelector('[data-hydration-marker]') !== null;
}
private performHydration(): void {
if (isHydratable(HydratableComponent.template)) {
// Get existing content range
const firstChild = this.shadowRoot?.firstChild;
const lastChild = this.shadowRoot?.lastChild;
if (firstChild && lastChild) {
// Perform hydration
const view = HydratableComponent.template.hydrate(
firstChild,
lastChild,
this
);
// Bind to current component data
view.bind(this);
}
}
}
}
// SSR rendering utility
class SSRRenderer {
static renderComponentToString<T>(
template: HydratableElementViewTemplate<T>,
data: T
): string {
if (!isHydratable(template)) {
throw new Error('Template must be hydratable for SSR');
}
// Server-side rendering logic
return this.renderTemplateWithData(template, data);
}
private static renderTemplateWithData<T>(
template: HydratableElementViewTemplate<T>,
data: T
): string {
// Simplified SSR rendering
// In real implementation, this would involve proper template processing
const title = (data as any).title || 'Default Title';
const description = (data as any).description || 'Default Description';
return `
<div class="hydratable-content" data-ssr="true">
<!-- ${HydrationMarkup.contentBindingStartMarker(0, 'title')} -->
<h1>${title}</h1>
<!-- ${HydrationMarkup.contentBindingEndMarker(0)} -->
<!-- ${HydrationMarkup.contentBindingStartMarker(1, 'description')} -->
<p>${description}</p>
<!-- ${HydrationMarkup.contentBindingEndMarker(1)} -->
</div>
`;
}
}
// Client-side hydration orchestrator
class HydrationOrchestrator {
private hydrationQueue: Array<{
element: Element;
component: any;
template: HydratableElementViewTemplate<any>;
}> = [];
queueForHydration<T>(
element: Element,
component: T,
template: HydratableElementViewTemplate<T>
): void {
if (!isHydratable(template)) {
console.warn('Template is not hydratable, skipping hydration queue');
return;
}
this.hydrationQueue.push({ element, component, template });
}
async processHydrationQueue(): Promise<void> {
for (const item of this.hydrationQueue) {
try {
await this.hydrateComponent(item);
} catch (error) {
console.error('Hydration failed for component:', error);
}
}
this.hydrationQueue = [];
}
private async hydrateComponent(item: {
element: Element;
component: any;
template: HydratableElementViewTemplate<any>;
}): Promise<void> {
const { element, component, template } = item;
// Find hydration range
const shadowRoot = element.shadowRoot;
if (!shadowRoot) {
throw new Error('Element must have shadow root for hydration');
}
const firstChild = shadowRoot.firstChild;
const lastChild = shadowRoot.lastChild;
if (firstChild && lastChild) {
// Perform hydration
const view = template.hydrate(firstChild, lastChild, element);
// Bind to component data
view.bind(component);
// Mark as hydrated
element.setAttribute('data-hydrated', 'true');
}
}
}
// Application setup with SSR hydration
class SSRApp {
private hydrationOrchestrator = new HydrationOrchestrator();
async initializeWithHydration(): Promise<void> {
// Find all server-rendered components
const ssrElements = document.querySelectorAll('[data-ssr]');
for (const element of ssrElements) {
await this.setupComponentHydration(element);
}
// Process all queued hydrations
await this.hydrationOrchestrator.processHydrationQueue();
}
private async setupComponentHydration(element: Element): Promise<void> {
const tagName = element.tagName.toLowerCase();
// Map element to component class and template
const componentInfo = this.getComponentInfo(tagName);
if (!componentInfo) {
return;
}
const { componentClass, template } = componentInfo;
// Create component instance
const component = new componentClass();
// Extract data from SSR attributes or content
this.populateComponentFromSSR(component, element);
// Queue for hydration
this.hydrationOrchestrator.queueForHydration(
element,
component,
template
);
}
private getComponentInfo(tagName: string): {
componentClass: any;
template: HydratableElementViewTemplate<any>;
} | null {
// Component registry lookup
const registry: Record<string, any> = {
'hydratable-component': {
componentClass: HydratableComponent,
template: HydratableComponent.template
}
};
return registry[tagName] || null;
}
private populateComponentFromSSR(component: any, element: Element): void {
// Extract component data from SSR attributes or content
const title = element.querySelector('h1')?.textContent;
const description = element.querySelector('p')?.textContent;
if (title) component.title = title;
if (description) component.description = description;
}
}
// Initialize SSR hydration
const ssrApp = new SSRApp();
document.addEventListener('DOMContentLoaded', () => {
ssrApp.initializeWithHydration().then(() => {
console.log('SSR hydration completed');
}).catch(error => {
console.error('SSR hydration failed:', error);
});
});Error handling and debugging utilities for hydration processes, helping identify and resolve hydration mismatches.
/**
* Error thrown when hydration fails due to content mismatch
*/
class HydrationBindingError extends Error {
/**
* Creates a hydration binding error
* @param message - Error message
* @param propertyBag - Additional error information
*/
constructor(
message: string | undefined,
public readonly propertyBag: {
index: number;
hydrationStage: string;
itemsLength?: number;
viewsState: string[];
viewTemplateString?: string;
rootNodeContent: string;
}
);
}
/**
* Hydration validation utilities
*/
const HydrationValidator: {
/**
* Validates that server and client content match
* @param serverContent - Content from server-side rendering
* @param clientTemplate - Client-side template
* @param data - Data used for rendering
* @returns Validation result
*/
validateContentMatch(
serverContent: string,
clientTemplate: ViewTemplate,
data: any
): HydrationValidationResult;
/**
* Checks for common hydration issues
* @param element - Element being hydrated
* @returns Array of potential issues
*/
checkForHydrationIssues(element: Element): HydrationIssue[];
/**
* Enables debug mode for hydration
* @param enabled - Whether to enable debug mode
*/
setDebugMode(enabled: boolean): void;
};
/**
* Result of hydration validation
*/
interface HydrationValidationResult {
/** Whether validation passed */
isValid: boolean;
/** List of validation errors */
errors: HydrationValidationError[];
/** List of validation warnings */
warnings: HydrationValidationWarning[];
}
/**
* Hydration validation error
*/
interface HydrationValidationError {
/** Error type */
type: 'content-mismatch' | 'structure-mismatch' | 'missing-marker';
/** Error message */
message: string;
/** Expected content */
expected: string;
/** Actual content */
actual: string;
/** Location of error */
location: {
bindingIndex?: number;
elementPath?: string;
};
}
/**
* Hydration validation warning
*/
interface HydrationValidationWarning {
/** Warning type */
type: 'performance' | 'accessibility' | 'seo';
/** Warning message */
message: string;
/** Suggested fix */
suggestion?: string;
}
/**
* Potential hydration issue
*/
interface HydrationIssue {
/** Issue severity */
severity: 'error' | 'warning' | 'info';
/** Issue category */
category: 'content' | 'structure' | 'performance' | 'accessibility';
/** Issue description */
description: string;
/** Element causing the issue */
element: Element;
/** Suggested resolution */
resolution?: string;
}Usage Examples:
import {
HydrationBindingError,
HydrationValidator,
HydrationValidationResult
} from "@microsoft/fast-element";
// Hydration error handler
class HydrationErrorHandler {
static handleHydrationError(error: HydrationBindingError): void {
console.group('Hydration Error Details');
console.error('Message:', error.message);
console.log('Binding Index:', error.propertyBag.index);
console.log('Hydration Stage:', error.propertyBag.hydrationStage);
console.log('Views State:', error.propertyBag.viewsState);
console.log('Root Node Content:', error.propertyBag.rootNodeContent);
if (error.propertyBag.viewTemplateString) {
console.log('Template:', error.propertyBag.viewTemplateString);
}
console.groupEnd();
// Attempt recovery
this.attemptHydrationRecovery(error);
}
private static attemptHydrationRecovery(error: HydrationBindingError): void {
// Recovery strategies based on error stage
switch (error.propertyBag.hydrationStage) {
case 'content-binding':
this.recoverContentBinding(error);
break;
case 'attribute-binding':
this.recoverAttributeBinding(error);
break;
case 'event-binding':
this.recoverEventBinding(error);
break;
default:
console.warn('No recovery strategy available for stage:', error.propertyBag.hydrationStage);
}
}
private static recoverContentBinding(error: HydrationBindingError): void {
console.log('Attempting content binding recovery...');
// Implement content binding recovery logic
}
private static recoverAttributeBinding(error: HydrationBindingError): void {
console.log('Attempting attribute binding recovery...');
// Implement attribute binding recovery logic
}
private static recoverEventBinding(error: HydrationBindingError): void {
console.log('Attempting event binding recovery...');
// Implement event binding recovery logic
}
}
// Hydration debugging utility
class HydrationDebugger {
private static debugMode = false;
static enableDebugMode(): void {
this.debugMode = true;
HydrationValidator.setDebugMode(true);
console.log('Hydration debug mode enabled');
}
static disableDebugMode(): void {
this.debugMode = false;
HydrationValidator.setDebugMode(false);
console.log('Hydration debug mode disabled');
}
static validateAndLog(
serverContent: string,
clientTemplate: ViewTemplate,
data: any,
elementName: string
): boolean {
if (!this.debugMode) return true;
console.group(`Hydration Validation: ${elementName}`);
try {
const result = HydrationValidator.validateContentMatch(
serverContent,
clientTemplate,
data
);
this.logValidationResult(result);
return result.isValid;
} catch (error) {
console.error('Validation failed:', error);
return false;
} finally {
console.groupEnd();
}
}
private static logValidationResult(result: HydrationValidationResult): void {
if (result.isValid) {
console.log('✅ Hydration validation passed');
} else {
console.log('❌ Hydration validation failed');
}
if (result.errors.length > 0) {
console.group('Errors:');
result.errors.forEach((error, index) => {
console.error(`${index + 1}. ${error.type}: ${error.message}`);
console.log(' Expected:', error.expected);
console.log(' Actual:', error.actual);
if (error.location.bindingIndex !== undefined) {
console.log(' Binding Index:', error.location.bindingIndex);
}
if (error.location.elementPath) {
console.log(' Element Path:', error.location.elementPath);
}
});
console.groupEnd();
}
if (result.warnings.length > 0) {
console.group('Warnings:');
result.warnings.forEach((warning, index) => {
console.warn(`${index + 1}. ${warning.type}: ${warning.message}`);
if (warning.suggestion) {
console.log(' Suggestion:', warning.suggestion);
}
});
console.groupEnd();
}
}
static checkElementForIssues(element: Element): void {
if (!this.debugMode) return;
const issues = HydrationValidator.checkForHydrationIssues(element);
if (issues.length === 0) {
console.log('✅ No hydration issues found for element:', element.tagName);
return;
}
console.group(`Hydration Issues for ${element.tagName}:`);
issues.forEach((issue, index) => {
const icon = issue.severity === 'error' ? '❌' :
issue.severity === 'warning' ? '⚠️' : 'ℹ️';
console.log(`${icon} ${index + 1}. [${issue.category}] ${issue.description}`);
if (issue.resolution) {
console.log(` 💡 Resolution: ${issue.resolution}`);
}
});
console.groupEnd();
}
}
// Production hydration with error handling
class ProductionHydrationManager {
private errorReporter?: (error: any) => void;
constructor(errorReporter?: (error: any) => void) {
this.errorReporter = errorReporter;
}
async safeHydration<T>(
element: Element,
template: HydratableElementViewTemplate<T>,
data: T
): Promise<boolean> {
try {
// Validate before hydration in development
if (process.env.NODE_ENV === 'development') {
HydrationDebugger.enableDebugMode();
HydrationDebugger.checkElementForIssues(element);
}
// Perform hydration
const view = template.hydrate(
element.firstChild!,
element.lastChild!,
element
);
view.bind(data);
return true;
} catch (error) {
// Handle hydration errors
if (error instanceof HydrationBindingError) {
HydrationErrorHandler.handleHydrationError(error);
} else {
console.error('Unexpected hydration error:', error);
}
// Report error in production
this.errorReporter?.(error);
// Fallback to client-side rendering
return this.fallbackToCSR(element, template, data);
}
}
private async fallbackToCSR<T>(
element: Element,
template: HydratableElementViewTemplate<T>,
data: T
): Promise<boolean> {
try {
console.log('Falling back to client-side rendering');
// Clear server-rendered content
element.innerHTML = '';
// Create fresh client-side view
const view = template.create(element);
view.bind(data);
view.appendTo(element);
return true;
} catch (error) {
console.error('Client-side rendering fallback failed:', error);
this.errorReporter?.(error);
return false;
}
}
}
// Application-level hydration setup
class AppHydration {
private productionManager = new ProductionHydrationManager(
(error) => {
// Report to error tracking service
console.error('Hydration error reported:', error);
}
);
async hydrateApplication(): Promise<void> {
const hydratableElements = document.querySelectorAll('[data-hydratable]');
const hydrationPromises = Array.from(hydratableElements).map(element =>
this.hydrateElement(element)
);
const results = await Promise.allSettled(hydrationPromises);
// Log overall hydration results
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`Hydration complete: ${successful} successful, ${failed} failed`);
if (failed > 0) {
console.warn('Some components failed to hydrate and fell back to CSR');
}
}
private async hydrateElement(element: Element): Promise<void> {
const componentType = element.getAttribute('data-component-type');
const componentData = this.extractComponentData(element);
// Get component template (would be from registry in real app)
const template = this.getComponentTemplate(componentType!);
if (template && isHydratable(template)) {
const success = await this.productionManager.safeHydration(
element,
template,
componentData
);
if (!success) {
throw new Error(`Failed to hydrate ${componentType}`);
}
}
}
private extractComponentData(element: Element): any {
// Extract component data from element attributes or content
const dataScript = element.querySelector('script[type="application/json"]');
if (dataScript) {
return JSON.parse(dataScript.textContent || '{}');
}
return {};
}
private getComponentTemplate(componentType: string): HydratableElementViewTemplate<any> | null {
// Component template registry
const templates: Record<string, any> = {
'user-card': createHydratableTemplate(),
'product-listing': createHydratableTemplate(),
// Add more component templates
};
return templates[componentType] || null;
}
}/**
* Hydration stage type
*/
type HydrationStage =
| "initial"
| "content-binding"
| "attribute-binding"
| "event-binding"
| "complete";
/**
* Hydration marker type
*/
interface HydrationMarker {
/** Marker type */
type: 'content' | 'attribute' | 'element';
/** Binding index */
index: number;
/** Target node ID */
targetNodeId?: string;
/** Attribute name for attribute markers */
attributeName?: string;
}
/**
* Hydration context interface
*/
interface HydrationContext {
/** Current hydration stage */
stage: HydrationStage;
/** Available hydration markers */
markers: HydrationMarker[];
/** Root element being hydrated */
rootElement: Element;
/** Component data */
data: any;
}