DEV Community

Pinoy Codie
Pinoy Codie

Posted on

Complete Migration Guide: Marko to Juris Universal Islands

Transitioning from Marko's server-side template system to Juris's universal JavaScript islands with integrated server-side rendering

Performance Analysis: Real-World Load Testing Results

Juris's universal SSR implementation demonstrates superior performance metrics compared to traditional template-based frameworks:

Load Testing Specifications:

Configuration: 2000 virtual users, 10 connections per user (20,000 total requests) Test Duration: 11 seconds 🎯 PERFORMANCE METRICS: • Request Rate: 1,988 requests/second • Median Response Time: 5ms • P95 Response Time: 7.9ms • P99 Response Time: 10.9ms • Success Rate: 100% (zero failures) • Data Throughput: 26.78MB processed 🔥 STANDOUT RESULTS: • Sub-5ms median response times • 2000+ requests/second sustained performance • Zero request failures under peak load • Consistent performance across all percentiles 
Enter fullscreen mode Exit fullscreen mode

Framework Performance Comparison:

Framework Median Response P95 Response Throughput Build Step Required
Juris Universal 5ms 7.9ms 1,988 req/s ❌ None
Marko SSR ~15-25ms ~45-60ms ~800-1200 req/s ✅ Required
Next.js SSR ~20-35ms ~80-120ms ~600-900 req/s ✅ Required
Nuxt.js SSR ~25-40ms ~90-150ms ~500-800 req/s ✅ Required

Performance Optimization Factors:

1. Elimination of Build Step Overhead

  • Direct JavaScript execution without compilation
  • No template processing delays
  • Zero bundling latency impact

2. Singleton Architecture Optimization

// Single application instance shared across requests const app = createApp(); function resetAppForRequest() { app.stateManager.reset([]); app.stateManager.state = INITIAL_STATE; return app; } 
Enter fullscreen mode Exit fullscreen mode

3. Pre-optimized HTML Templates

// Template parts compiled once at startup, not per-request const HTML_HEAD = `<!DOCTYPE html>...`; const HTML_TAIL = `</div><script>window.__hydration_data = `; const HTML_FOOTER = `;</script></body></html>`; 
Enter fullscreen mode Exit fullscreen mode

4. Streamlined String Rendering

  • Direct string concatenation approach
  • Minimal virtual DOM overhead
  • Optimized memory allocation patterns

Resource Efficiency Metrics:

  • Memory Footprint: Single application instance approach
  • Garbage Collection: Efficient string-based rendering
  • Memory Leaks: Prevented through proper state reset mechanisms

Universal Debugging Architecture: Debug Anywhere Capabilities

Juris's most innovative feature is Debug Anywhere - enabling identical debugging interfaces whether components execute server-side during SSR or client-side during hydration and interaction.

Universal Debugging Interface Implementation

// components/todo-list.js - Universal debugging support function createTodoList(juris) { const component = { // Debugging interface that works across environments debug: { // Functions identically on server and client getState: () => ({ todos: juris.getState('todos.items', []), newTodo: juris.getState('todos.newTodo', ''), environment: typeof window !== 'undefined' ? 'client' : 'server', timestamp: new Date().toISOString() }), logAction: (action, data) => { const env = typeof window !== 'undefined' ? 'CLIENT' : 'SERVER'; console.log(`[${env}] TodoList.${action}:`, data); }, logRender: (context, meta) => { const env = typeof window !== 'undefined' ? 'CLIENT' : 'SERVER'; console.log(`[${env}] TodoList.render(${context}):`, meta); }, // Environment-adaptive inspection inspect: () => { if (typeof window !== 'undefined') { // Client-side debugging capabilities return { environment: 'client', domElements: document.querySelectorAll('[data-debug-component="todo-list"]').length, enhancements: juris.getEnhancementStats?.() || 'Not available', state: component.debug.getState() }; } else { // Server-side debugging capabilities return { environment: 'server', renderMode: 'ssr', state: component.debug.getState() }; } } }, // Component methods with integrated debug hooks addTodo: (text) => { const todos = juris.getState('todos.items', []); const newTodo = { id: Date.now(), text, done: false }; juris.setState('todos.items', [...todos, newTodo]); // Universal debug hook component.debug.logAction('addTodo', { text, newTodo }); }, toggleTodo: (id) => { const todos = juris.getState('todos.items', []); const updated = todos.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo ); juris.setState('todos.items', updated); // Universal debug hook component.debug.logAction('toggleTodo', { id, affected: updated.filter(t => t.id === id) }); }, // SERVER-SIDE: String rendering with debug information renderToString: () => { const renderStart = Date.now(); const todos = juris.getState('todos.items', []); // Debug during server-side rendering component.debug.logRender('server', { todoCount: todos.length, renderTime: Date.now() - renderStart }); const html = ` <div class="todo-list" data-debug-component="todo-list" data-debug-env="server"> <h3>Todos (${todos.length})</h3> <ul class="todo-items"> ${todos.map(todo => ` <li data-todo-id="${todo.id}" class="${todo.done ? 'done' : ''}"> <span>${todo.text}</span> <button class="toggle-btn">${todo.done ? 'Undo' : 'Done'}</button> <button class="delete-btn">Delete</button> </li> `).join('')} </ul> <div class="add-todo"> <input class="new-todo-input" value="${juris.getState('todos.newTodo', '')}" placeholder="Add todo"> <button class="add-todo-btn">Add</button> </div> </div> `; // Debug the final rendered output component.debug.logRender('server-complete', { htmlLength: html.length }); return html; }, // CLIENT-SIDE: Enhancement with debug information enhance: () => { const enhanceStart = Date.now(); // Debug enhancement initialization component.debug.logRender('client-enhance-start', { enhanceStart, existingElements: document.querySelectorAll('[data-debug-component="todo-list"]').length }); // Input field enhancement juris.enhance('.new-todo-input', { value: () => juris.getState('todos.newTodo', ''), oninput: (e) => { juris.setState('todos.newTodo', e.target.value); component.debug.logAction('input-change', { value: e.target.value }); }, onkeyup: (e, { todoManager }) => { if (e.key === 'Enter') { component.addTodo(e.target.value); } } }); // Add button enhancement juris.enhance('.add-todo-btn', { onclick: () => { const newTodo = juris.getState('todos.newTodo', ''); component.addTodo(newTodo); } }); // Dynamic list enhancement juris.enhance('.todo-items', { children: () => { const todos = juris.getState('todos.items', []); component.debug.logRender('client-list-update', { todoCount: todos.length }); return todos.map(todo => ({ li: { key: todo.id, className: todo.done ? 'done' : '', 'data-todo-id': todo.id, children: [ { span: { text: todo.text } }, { button: { className: 'toggle-btn', text: todo.done ? 'Undo' : 'Done', onclick: () => component.toggleTodo(todo.id) } }, { button: { className: 'delete-btn', text: 'Delete', onclick: () => { const todos = juris.getState('todos.items', []); juris.setState('todos.items', todos.filter(t => t.id !== todo.id)); component.debug.logAction('deleteTodo', { id: todo.id }); } } } ] } })); } }); // Debug enhancement completion component.debug.logRender('client-enhance-complete', { enhanceTime: Date.now() - enhanceStart }); } }; return component; } 
Enter fullscreen mode Exit fullscreen mode

Debugging Usage Examples

Server-Side Debugging During SSR:

// server.js - Server-side debug integration fastify.get('*', async (request, reply) => { const serverApp = resetAppForRequest(); // Instantiate todo component const todoList = createTodoList(serverApp); // Server-side debugging console.log('SSR Debug:', todoList.debug.inspect()); // Output: { environment: 'server', renderMode: 'ssr', state: {...} } // Render with debug tracking const content = todoList.renderToString(); // Console: [SERVER] TodoList.render(server): { todoCount: 2, renderTime: 1 } return htmlTemplate(content, serverApp.stateManager.state); }); 
Enter fullscreen mode Exit fullscreen mode

Client-Side Debugging During Hydration:

// Client-side debug integration const clientApp = createApp(); const todoList = createTodoList(clientApp); // Client-side debugging console.log('Client Debug:', todoList.debug.inspect()); // Output: { environment: 'client', domElements: 1, enhancements: {...}, state: {...} } // Enhancement with debug tracking todoList.enhance(); // Console: [CLIENT] TodoList.render(client-enhance-start): { enhanceStart: 1234567890 } // Debug component interactions todoList.addTodo('Debug this functionality!'); // Console: [CLIENT] TodoList.addTodo: { text: 'Debug this functionality!', newTodo: {...} } 
Enter fullscreen mode Exit fullscreen mode

Universal Debug Console Implementation

// Global debug console for all environments window.debugTodoList = () => { const todoList = createTodoList(window.clientApp || serverApp); return { // Universal debugging methods inspect: () => todoList.debug.inspect(), getState: () => todoList.debug.getState(), // Component testing methods addTestTodo: () => todoList.addTodo('Debug test todo'), toggleFirst: () => { const todos = todoList.debug.getState().todos; if (todos.length > 0) { todoList.toggleTodo(todos[0].id); } }, // Environment detection whereAmI: () => { return typeof window !== 'undefined' ? 'CLIENT' : 'SERVER'; } }; }; // Browser console usage examples: // debugTodoList().inspect() // debugTodoList().addTestTodo() // debugTodoList().whereAmI() 
Enter fullscreen mode Exit fullscreen mode

Debug Anywhere vs Marko Debugging Comparison

Traditional Marko Debugging:

<!-- components/todo-list/index.marko --> <div class="todo-list"> <!-- Limited template debugging --> <div if(process.env.NODE_ENV === 'development')> Debug: ${JSON.stringify(state)} </div> </div> <script> // Environment-specific debugging export default { onCreate() { if (process.env.NODE_ENV === 'development') { console.log('Marko component created'); } } // Separate debugging approaches for server vs client }; </script> 
Enter fullscreen mode Exit fullscreen mode

Juris Universal Debugging:

// Unified debugging interface across environments const todoList = createTodoList(juris); // Identical API for server-side rendering console.log('SSR State:', todoList.debug.getState()); // Identical API for client-side hydration console.log('Client State:', todoList.debug.getState()); // Universal debugging methods work everywhere todoList.debug.inspect(); // Environment-aware inspection 
Enter fullscreen mode Exit fullscreen mode

Debug Anywhere Architecture Benefits:

  1. Unified Debug Interface: Identical debugging API across server and client environments
  2. Environment Intelligence: Debugging automatically adapts to execution context
  3. State Transparency: Component state accessible in all environments
  4. Action Tracking: Monitor component behavior across environment boundaries
  5. Performance Monitoring: Measure SSR vs client enhancement performance metrics
  6. Production Safety: Debug functionality can be conditionally enabled

This Debug Anywhere Architecture eliminates the traditional debugging friction between server-side rendering and client-side enhancement, providing a seamless debugging experience throughout the entire application lifecycle.


Migration Rationale: Why Choose Juris Over Marko?

While Marko excels at server-side rendering through its template-based architecture, Juris provides a more cohesive and maintainable development approach:

Marko Framework Limitations:

  • Template-Logic Division - Business logic fragmented between template files and JavaScript
  • Build Tool Complexity - Dependencies on Marko compiler and associated build infrastructure
  • Dual Syntax Requirements - Developers must master both template syntax and JavaScript patterns
  • Client-Side Constraints - Template-centric approach restricts dynamic client behavior flexibility
  • Vendor Lock-in - Marko-specific template syntax creates migration barriers

Juris Framework Advantages:

  • Universal JavaScript Approach - Identical codebase executes on server and client
  • Zero Build Dependencies - Direct compatibility with Node.js and browser environments
  • Authentic SSR + Hydration - Server renders initial content, client enhances progressively
  • Islands Architecture - Progressive enhancement through isolated component systems
  • Pure JavaScript Foundation - No proprietary template syntax requirements

Architectural Pattern Comparison

Marko Processing Flow:

Server: .marko templates → Marko Compiler → Compiled JavaScript → HTML Output Client: Marko Hydrated Components 
Enter fullscreen mode Exit fullscreen mode

Juris Processing Flow:

Server: Juris Application → String Renderer → HTML + State Output Client: Same Juris Application → State Hydration with Enhancement 
Enter fullscreen mode Exit fullscreen mode

Phase 1: Server-Side Rendering Implementation Differences

Marko SSR Configuration:

// server.js (Marko implementation) const express = require('express'); const markoExpress = require('@marko/express'); const homeTemplate = require('./views/home.marko'); app.use(markoExpress()); app.get('/', (req, res) => { res.marko(homeTemplate, { title: 'Home Page', todos: [ { id: 1, text: 'Learn Marko Framework', done: false } ] }); }); 
Enter fullscreen mode Exit fullscreen mode

Juris SSR Configuration:

// server.js (Juris with Universal Components) const fastify = require('fastify')({ logger: false }); const Juris = require('./juris/juris.js'); const { createApp } = require('./source/app.js'); // Server-side DOM environment setup if (!global.document) { global.document = { createElement: () => ({}), querySelector: () => null, querySelectorAll: () => [] }; } // Application singleton creation const app = createApp(); const stringRenderer = app.getHeadlessComponent('StringRenderer').api; stringRenderer.enableStringRenderer(); // HTML template generation function function htmlTemplate(content, state) { return `<!DOCTYPE html> <html> <head> <title>Juris Universal Application</title> <script src="https://unpkg.com/juris@0.5.2/juris.js"></script> </head> <body> <div id="app">${content}</div> <script> window.__hydration_data = ${JSON.stringify(state)}; // Automatic client-side hydration const clientApp = createApp(); clientApp.render('#app'); </script> </body> </html>`; } // Universal request handler fastify.get('*', async (request, reply) => { try { // Request-specific app state reset app.stateManager.reset([]); app.stateManager.state = { todos: [ { id: 1, text: 'Learn Juris Framework', done: false } ] }; // Route configuration const router = app.getHeadlessComponent('Router').api; router.setRoute(request.url); // String rendering execution const content = stringRenderer.renderToString(); const state = app.stateManager.state; reply.type('text/html'); return htmlTemplate(content, state); } catch (error) { reply.code(500); return `<h1>Error</h1><p>${error.message}</p>`; } }); 
Enter fullscreen mode Exit fullscreen mode

Phase 2: Component Migration Strategies

2.1 Basic Interactive Components

Marko Counter Template:

<!-- components/counter/index.marko --> <div class="counter"> <h2>Counter: ${state.count}</h2> <button on-click('increment')>Increment</button> <button on-click('decrement')>Decrement</button> </div> <script> export default { onCreate() { this.state = { count: 0 }; }, increment() { this.state.count++; }, decrement() { this.state.count--; } }; </script> 
Enter fullscreen mode Exit fullscreen mode

Juris Universal Counter Component:

// components/counter.js (Universal execution) function createCounter(juris) { return { // Server-side string rendering capability renderToString: () => { const count = juris.getState('counter.count', 0); return ` <div class="counter"> <h2>Counter: ${count}</h2> <button class="increment-btn">Increment</button> <button class="decrement-btn">Decrement</button> </div> `; }, // Client-side progressive enhancement enhance: () => { juris.enhance('.increment-btn', { onclick: () => { const current = juris.getState('counter.count', 0); juris.setState('counter.count', current + 1); } }); juris.enhance('.decrement-btn', { onclick: () => { const current = juris.getState('counter.count', 0); juris.setState('counter.count', current - 1); } }); // Reactive display updates juris.enhance('.counter h2', { text: () => `Counter: ${juris.getState('counter.count', 0)}` }); } }; } module.exports = { createCounter }; 
Enter fullscreen mode Exit fullscreen mode

2.2 Dynamic List Components with Server Rendering

Marko Todo List Template:

<!-- components/todo-list/index.marko --> <div class="todo-list"> <h3>Todos (${state.todos.length})</h3> <ul> <li for(todo in state.todos) key=todo.id class=todo.done ? 'done' : ''> <span>${todo.text}</span> <button on-click('toggleTodo', todo.id)> ${todo.done ? 'Undo' : 'Done'} </button> <button on-click('deleteTodo', todo.id)>Delete</button> </li> </ul> <div class="add-todo"> <input key="newTodo" bind-value="newTodo" placeholder="Add todo"> <button on-click('addTodo')>Add</button> </div> </div> <script> export default { onCreate() { this.state = { todos: this.input.todos || [], newTodo: '' }; }, toggleTodo(id) { const todo = this.state.todos.find(t => t.id === id); todo.done = !todo.done; }, deleteTodo(id) { this.state.todos = this.state.todos.filter(t => t.id !== id); }, addTodo() { if (this.state.newTodo.trim()) { this.state.todos.push({ id: Date.now(), text: this.state.newTodo, done: false }); this.state.newTodo = ''; } } }; </script> 
Enter fullscreen mode Exit fullscreen mode

Juris Universal Todo List Component:

// components/todo-list.js function createTodoList(juris) { const services = { todoManager: { addTodo: () => { const newTodo = juris.getState('todos.newTodo', ''); if (newTodo.trim()) { const todos = juris.getState('todos.items', []); juris.setState('todos.items', [ ...todos, { id: Date.now(), text: newTodo, done: false } ]); juris.setState('todos.newTodo', ''); } }, toggleTodo: (id) => { const todos = juris.getState('todos.items', []); const updated = todos.map(todo => todo.id === id ? { ...todo, done: !todo.done } : todo ); juris.setState('todos.items', updated); }, deleteTodo: (id) => { const todos = juris.getState('todos.items', []); juris.setState('todos.items', todos.filter(t => t.id !== id)); } } }; return { // Service registration registerServices: () => { Object.assign(juris.services, services); }, // Server-side rendering implementation renderToString: () => { const todos = juris.getState('todos.items', []); const newTodo = juris.getState('todos.newTodo', ''); const todoItems = todos.map(todo => ` <li class="${todo.done ? 'done' : ''}" data-todo-id="${todo.id}"> <span>${todo.text}</span> <button class="toggle-btn">${todo.done ? 'Undo' : 'Done'}</button> <button class="delete-btn">Delete</button> </li> `).join(''); return ` <div class="todo-list"> <h3>Todos (${todos.length})</h3> <ul class="todo-items">${todoItems}</ul> <div class="add-todo"> <input class="new-todo-input" value="${newTodo}" placeholder="Add todo"> <button class="add-todo-btn">Add</button> </div> </div> `; }, // Client-side enhancement implementation enhance: () => { // Input field enhancement juris.enhance('.new-todo-input', { value: () => juris.getState('todos.newTodo', ''), oninput: (e) => juris.setState('todos.newTodo', e.target.value), onkeyup: (e, { todoManager }) => { if (e.key === 'Enter') { todoManager.addTodo(); } } }); // Add button enhancement juris.enhance('.add-todo-btn', { onclick: ({ todoManager }) => todoManager.addTodo() }); // Dynamic list rendering juris.enhance('.todo-items', ({ todoManager }) => ({ children: () => { const todos = juris.getState('todos.items', []); return todos.map(todo => ({ li: { key: todo.id, className: todo.done ? 'done' : '', children: [ { span: { text: todo.text } }, { button: { className: 'toggle-btn', text: todo.done ? 'Undo' : 'Done', onclick: () => todoManager.toggleTodo(todo.id) } }, { button: { className: 'delete-btn', text: 'Delete', onclick: () => todoManager.deleteTodo(todo.id) } } ] } })); } })); // Counter updates juris.enhance('.todo-list h3', { text: () => { const todos = juris.getState('todos.items', []); return `Todos (${todos.length})`; } }); } }; } module.exports = { createTodoList }; 
Enter fullscreen mode Exit fullscreen mode

2.3 Advanced Form Components with Validation

Marko Contact Form:

<!-- components/contact-form/index.marko --> <form class="contact-form" on-submit('handleSubmit')> <div class="field"> <label>Name</label> <input bind-value="name" class=state.errors.name ? 'error' : ''> <span if(state.errors.name) class="error">${state.errors.name}</span> </div> <div class="field"> <label>Email</label> <input type="email" bind-value="email" class=state.errors.email ? 'error' : ''> <span if(state.errors.email) class="error">${state.errors.email}</span> </div> <button type="submit" disabled=!state.isValid> ${state.isSubmitting ? 'Sending...' : 'Send Message'} </button> </form> <script> export default { onCreate() { this.state = { name: '', email: '', errors: {}, isSubmitting: false, isValid: false }; }, validate() { const errors = {}; if (!this.state.name.trim()) errors.name = 'Name required'; if (!this.state.email.includes('@')) errors.email = 'Valid email required'; this.state.errors = errors; this.state.isValid = Object.keys(errors).length === 0; }, async handleSubmit(event) { event.preventDefault(); this.validate(); if (this.state.isValid) { this.state.isSubmitting = true; await this.submitForm(); this.state.isSubmitting = false; } } }; </script> 
Enter fullscreen mode Exit fullscreen mode

Juris Universal Contact Form:

// components/contact-form.js function createContactForm(juris) { const services = { formValidator: { validateField: (field, value) => { let error = null; if (field === 'name' && !value.trim()) { error = 'Name required'; } else if (field === 'email' && !value.includes('@')) { error = 'Valid email required'; } juris.setState(`contactForm.errors.${field}`, error); return !error; }, isFormValid: () => { const errors = juris.getState('contactForm.errors', {}); const name = juris.getState('contactForm.name', ''); const email = juris.getState('contactForm.email', ''); return !errors.name && !errors.email && name && email; } }, formHandler: { submit: async () => { const { formValidator } = juris.services; const name = juris.getState('contactForm.name', ''); const email = juris.getState('contactForm.email', ''); // Complete field validation const nameValid = formValidator.validateField('name', name); const emailValid = formValidator.validateField('email', email); if (nameValid && emailValid) { juris.setState('contactForm.isSubmitting', true); try { // API submission simulation await new Promise(resolve => setTimeout(resolve, 1000)); console.log('Form submitted:', { name, email }); // Form state reset juris.setState('contactForm.name', ''); juris.setState('contactForm.email', ''); juris.setState('contactForm.errors', {}); } catch (error) { console.error('Submit error:', error); } finally { juris.setState('contactForm.isSubmitting', false); } } } } }; return { registerServices: () => { Object.assign(juris.services, services); }, renderToString: () => { const name = juris.getState('contactForm.name', ''); const email = juris.getState('contactForm.email', ''); const errors = juris.getState('contactForm.errors', {}); const isSubmitting = juris.getState('contactForm.isSubmitting', false); return ` <form class="contact-form"> <div class="field"> <label>Name</label> <input name="name" value="${name}" class="${errors.name ? 'error' : ''}"> <span class="error-message" data-field="name" style="display: ${errors.name ? 'block' : 'none'}">${errors.name || ''}</span> </div> <div class="field"> <label>Email</label> <input name="email" type="email" value="${email}" class="${errors.email ? 'error' : ''}"> <span class="error-message" data-field="email" style="display: ${errors.email ? 'block' : 'none'}">${errors.email || ''}</span> </div> <button type="submit" class="submit-btn"> ${isSubmitting ? 'Sending...' : 'Send Message'} </button> </form> `; }, enhance: () => { // Name input enhancement juris.enhance('input[name="name"]', { value: () => juris.getState('contactForm.name', ''), oninput: (e) => juris.setState('contactForm.name', e.target.value), onblur: ({ formValidator }) => { const value = juris.getState('contactForm.name', ''); formValidator.validateField('name', value); }, className: () => juris.getState('contactForm.errors.name') ? 'error' : '' }); // Email input enhancement juris.enhance('input[name="email"]', { value: () => juris.getState('contactForm.email', ''), oninput: (e) => juris.setState('contactForm.email', e.target.value), onblur: ({ formValidator }) => { const value = juris.getState('contactForm.email', ''); formValidator.validateField('email', value); }, className: () => juris.getState('contactForm.errors.email') ? 'error' : '' }); // Error message handling juris.enhance('.error-message', (ctx) => { const field = ctx.element.dataset.field; return { text: () => juris.getState(`contactForm.errors.${field}`, ''), style: () => ({ display: juris.getState(`contactForm.errors.${field}`) ? 'block' : 'none' }) }; }); // Submit button enhancement juris.enhance('.submit-btn', { disabled: ({ formValidator }) => !formValidator.isFormValid(), text: () => { const isSubmitting = juris.getState('contactForm.isSubmitting', false); return isSubmitting ? 'Sending...' : 'Send Message'; }, onclick: (e, { formHandler }) => { e.preventDefault(); formHandler.submit(); } }); } }; } module.exports = { createContactForm }; 
Enter fullscreen mode Exit fullscreen mode

Phase 3: Application Architecture Migration

3.1 Marko Application Structure:

marko-application/ ├── components/ │ ├── header/ │ │ ├── index.marko │ │ └── style.css │ ├── footer/ │ │ ├── index.marko │ │ └── style.css │ └── todo-list/ │ ├── index.marko │ └── component.js ├── views/ │ ├── home.marko │ ├── about.marko │ └── layout.marko ├── server.js └── package.json 
Enter fullscreen mode Exit fullscreen mode

3.2 Juris Universal Application Structure:

juris-application/ ├── components/ │ ├── header.js │ ├── footer.js │ ├── todo-list.js │ └── contact-form.js ├── source/ │ ├── app.js (universal application) │ └── router.js ├── public/ │ ├── css/ │ │ └── styles.css │ └── js/ │ └── juris-app.js (client bundle) ├── server.js (SSR server) └── package.json 
Enter fullscreen mode Exit fullscreen mode

3.3 Universal Application Configuration:

source/app.js (Universal Execution):

// source/app.js - Compatible with both server and client environments const Juris = require('../juris/juris.js'); // Node.js environment // const Juris = window.Juris; // Browser environment (conditional loading) const { createCounter } = require('../components/counter.js'); const { createTodoList } = require('../components/todo-list.js'); const { createContactForm } = require('../components/contact-form.js'); function createApp() { const juris = new Juris({ states: { counter: { count: 0 }, todos: { items: [], newTodo: '' }, contactForm: { name: '', email: '', errors: {}, isSubmitting: false }, router: { currentRoute: '/' } }, // Headless components for universal functionality headlessComponents: { // Server-side string rendering capability StringRenderer: (props, ctx) => ({ api: { enableStringRenderer: () => { ctx.juris.isServerSide = true; }, renderToString: () => { const route = ctx.getState('router.currentRoute', '/'); switch (route) { case '/': return createHomePage(ctx.juris); case '/about': return createAboutPage(ctx.juris); case '/todos': return createTodosPage(ctx.juris); case '/contact': return createContactPage(ctx.juris); default: return '<h1>404 - Page Not Found</h1>'; } } } }), // Universal routing system Router: (props, ctx) => ({ api: { setRoute: (path) => { ctx.setState('router.currentRoute', path); }, navigate: (path) => { ctx.setState('router.currentRoute', path); if (typeof window !== 'undefined') { window.history.pushState({}, '', path); } } } }) } }); // Component service registration const counter = createCounter(juris); const todoList = createTodoList(juris); const contactForm = createContactForm(juris); todoList.registerServices(); contactForm.registerServices(); // Client-side specific enhancements if (typeof window !== 'undefined') { // State hydration from server if (window.__hydration_data) { juris.stateManager.state = window.__hydration_data; } // Client-side routing setup const router = juris.getHeadlessComponent('Router').api; window.addEventListener('popstate', () => { router.setRoute(window.location.pathname); }); // Component enhancement activation counter.enhance(); todoList.enhance(); contactForm.enhance(); // Route-based content rendering juris.enhance('#app', { children: () => { const route = juris.getState('router.currentRoute', '/'); switch (route) { case '/': return [{ div: { innerHTML: createHomePage(juris) } }]; case '/about': return [{ div: { innerHTML: createAboutPage(juris) } }]; case '/todos': return [{ div: { innerHTML: createTodosPage(juris) } }]; case '/contact': return [{ div: { innerHTML: createContactPage(juris) } }]; default: return [{ h1: { text: '404 - Page Not Found' } }]; } } }); } return juris; } // Universal page generation functions function createHomePage(juris) { const counter = createCounter(juris); return ` <div class="home-page"> <h1>Welcome to Juris Universal Application</h1> <p>This page was rendered ${juris.isServerSide ? 'on the server' : 'on the client'}!</p> ${counter.renderToString()} <nav> <a href="/about">About</a> | <a href="/todos">Todos</a> | <a href="/contact">Contact</a> </nav> </div> `; } function createAboutPage(juris) { return ` <div class="about-page"> <h1>About This Application</h1> <p>This is a universal Juris application demonstrating server-side rendering capabilities.</p> <nav> <a href="/">Home</a> | <a href="/todos">Todos</a> | <a href="/contact">Contact</a> </nav> </div> `; } function createTodosPage(juris) { const todoList = createTodoList(juris); return ` <div class="todos-page"> <h1>Todo Management</h1> ${todoList.renderToString()} <nav> <a href="/">Home</a> | <a href="/about">About</a> | <a href="/contact">Contact</a> </nav> </div> `; } function createContactPage(juris) { const contactForm = createContactForm(juris); return ` <div class="contact-page"> <h1>Contact Information</h1> ${contactForm.renderToString()} <nav> <a href="/">Home</a> | <a href="/about">About</a> | <a href="/todos">Todos</a> </nav> </div> `; } // Multi-environment export compatibility if (typeof module !== 'undefined' && module.exports) { module.exports = { createApp }; } else { window.createApp = createApp; } 
Enter fullscreen mode Exit fullscreen mode

Phase 4: Optimized Server-Side Rendering Implementation

4.1 Production-Ready Fastify Server:

server.js (Performance-Optimized SSR Server):

// server.js - Production-optimized Fastify Server for Juris SSR const fastify = require('fastify')({ logger: false, disableRequestLogging: true, keepAliveTimeout: 30000, connectionTimeout: 60000, bodyLimit: 1048576, maxParamLength: 100, ignoreTrailingSlash: true, caseSensitive: false }); const path = require('path'); // Server-side DOM environment simulation if (!global.document) { global.document = { createElement: () => ({}), querySelector: () => null, querySelectorAll: () => [], addEventListener: () => {} }; } if (!global.window) { global.window = { addEventListener: () => {}, history: null, location: { pathname: '/', search: '' } }; } // Application dependencies const Juris = require('./juris/juris.js'); const { createApp } = require('./source/app.js'); const PORT = process.env.PORT || 3000; // Pre-compiled HTML template segments for performance const HTML_HEAD = `<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Juris Universal Application</title> <link rel="stylesheet" href="/public/css/styles.css"> <script src="https://unpkg.com/juris@0.5.2/juris.js"></script> </head> <body> <div id="app">`; const HTML_TAIL = `</div> <script> window.__hydration_data = `; const HTML_FOOTER = `; // Client-side application initialization const clientApp = createApp(); clientApp.render('#app'); </script> <script src="/public/js/juris-app.js"></script> </body> </html>`; // Static file serving with performance caching const staticOptions = { maxAge: '1d', immutable: true, etag: true, lastModified: true }; fastify.register(require('@fastify/static'), { root: path.join(__dirname, 'public'), prefix: '/public/', ...staticOptions }); // Singleton application instance creation const app = createApp(); const stringRenderer = app.getHeadlessComponent('StringRenderer').api; stringRenderer.enableStringRenderer(); // Default SSR state configuration const INITIAL_STATE = { counter: { count: 42 }, todos: { items: [ { id: 1, text: 'Server-rendered todo item', done: false }, { id: 2, text: 'Another example todo', done: true } ], newTodo: '' }, contactForm: { name: '', email: '', errors: {}, isSubmitting: false }, router: { currentRoute: '/' } }; // Optimized application reset mechanism function resetAppForRequest() { app.stateManager.reset([]); app.stateManager.state = { ...INITIAL_STATE }; return app; } // Efficient HTML template generation function htmlTemplate(content, state) { return HTML_HEAD + content + HTML_TAIL + JSON.stringify(state) + HTML_FOOTER; } // Route performance schema const routeSchema = { response: { 200: { type: 'string' }, 404: { type: 'string' }, 500: { type: 'string' } } }; // Static file detection pattern const STATIC_FILE_REGEX = /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/; // Universal request handler with optimization fastify.get('*', { schema: routeSchema }, async (request, reply) => { const url = request.url; // Performance fast-path for static file requests if (STATIC_FILE_REGEX.test(url)) { reply.code(404); return 'Not Found'; } try { // Request-scoped application reset const serverApp = resetAppForRequest(); // Component API access const router = serverApp.getHeadlessComponent('Router').api; const stringRenderer = serverApp.getHeadlessComponent('StringRenderer').api; // Server rendering configuration stringRenderer.enableStringRenderer(); router.setRoute(url); // String rendering execution const content = stringRenderer.renderToString(); // State extraction for client hydration const state = serverApp.stateManager.state; // Response optimization reply.type('text/html; charset=utf-8'); reply.header('Cache-Control', 'no-cache, no-store, must-revalidate'); return htmlTemplate(content, state); } catch (error) { console.error('SSR Processing Error:', error); reply.code(500); return `<h1>Server Error</h1><p>${error.message}</p>`; } }); // Response compression integration fastify.register(require('@fastify/compress'), { global: true, threshold: 1024, encodings: ['gzip', 'deflate'] }); // Server initialization const start = async () => { try { await fastify.listen({ port: PORT, host: '0.0.0.0', backlog: 1024, exclusive: false }); console.log(`🚀 Juris Universal Server running on http://localhost:${PORT}`); console.log('Enabled Features:'); console.log(' ✓ Server-side rendering capability'); console.log(' ✓ Client-side state hydration'); console.log(' ✓ Universal JavaScript components'); console.log(' ✓ Islands architecture implementation'); console.log(' ✓ Performance optimization suite'); } catch (err) { fastify.log.error(err); process.exit(1); } }; start(); 
Enter fullscreen mode Exit fullscreen mode

4.2 Client-Side Application Bundle:

public/js/juris-app.js:

// public/js/juris-app.js - Client-side application bundle // Production version would be generated from source/app.js via bundling // This enables universal app functionality in browser environment // Generated through build tools like esbuild, rollup, or webpack in production 
Enter fullscreen mode Exit fullscreen mode

Phase 5: Strategic Migration Implementation

5.1 Incremental Migration Approach:

Phase A: Parallel Framework Setup

// Maintain existing Marko functionality app.get('/legacy/*', (req, res) => { res.marko(legacyTemplate, data); }); // Introduce new Juris-powered routes app.get('/new/*', jurisHandler); 
Enter fullscreen mode Exit fullscreen mode

Phase B: Component-Level Migration Strategy

  1. Begin with simple interactive components (counters, toggles, basic forms)
  2. Progress to dynamic lists and complex state management
  3. Migrate sophisticated page layouts and navigation
  4. Complete with routing system migration

Phase C: Framework Consolidation

  1. Remove Marko framework dependencies
  2. Update build and deployment scripts
  3. Convert remaining templates to universal components

5.2 Migration Implementation Checklist:

  • [ ] Juris server setup with DOM environment simulation
  • [ ] Universal application architecture design
  • [ ] Simple component migration and testing
  • [ ] Server-side rendering integration
  • [ ] Client-side hydration implementation
  • [ ] SSR and client enhancement validation
  • [ ] Complex component migration
  • [ ] Routing system upgrade
  • [ ] Performance optimization implementation
  • [ ] Marko framework dependency removal

Phase 6: Testing and Quality Assurance

6.1 Universal Component Testing Framework:

// test/components.test.js const { createApp } = require('../source/app.js'); describe('Universal Component Testing', () => { let app; beforeEach(() => { // DOM environment simulation for testing global.document = { createElement: () => ({}), querySelector: () => null, querySelectorAll: () => [] }; app = createApp(); app.getHeadlessComponent('StringRenderer').api.enableStringRenderer(); }); test('Counter component server rendering', () => { app.setState('counter.count', 5); app.getHeadlessComponent('Router').api.setRoute('/'); const html = app.getHeadlessComponent('StringRenderer').api.renderToString(); expect(html).toContain('Counter: 5'); }); test('Todo list component with data rendering', () => { app.setState('todos.items', [ { id: 1, text: 'Test todo item', done: false } ]); app.getHeadlessComponent('Router').api.setRoute('/todos'); const html = app.getHeadlessComponent('StringRenderer').api.renderToString(); expect(html).toContain('Test todo item'); expect(html).toContain('Todos (1)'); }); }); 
Enter fullscreen mode Exit fullscreen mode

6.2 Server-Side Rendering Testing:

// test/ssr.test.js const fastify = require('fastify')(); const app = require('../server.js'); describe('Server-Side Rendering Validation', () => { test('Home page rendering with counter component', async () => { const response = await app.inject({ method: 'GET', url: '/' }); expect(response.statusCode).toBe(200); expect(response.body).toContain('Counter: 42'); expect(response.body).toContain('window.__hydration_data'); }); test('Todos page rendering with initial data', async () => { const response = await app.inject({ method: 'GET', url: '/todos' }); expect(response.statusCode).toBe(200); expect(response.body).toContain('Server-rendered todo item'); expect(response.body).toContain('Todo Management'); }); }); 
Enter fullscreen mode Exit fullscreen mode

Migration Benefits Analysis

Performance Advantages:

  • Build Pipeline Elimination - Direct Node.js execution without compilation overhead
  • Reduced Bundle Sizes - No Marko compiler dependencies in production
  • Faster Server Initialization - Elimination of template compilation step
  • Optimized Hydration Process - Minimal client-side JavaScript requirements

Developer Experience Improvements:

  • Universal JavaScript Development - Identical codebase for server and client
  • Enhanced Debugging Capabilities - Standard JavaScript debugging tools compatibility
  • Simplified Deployment Process - No build artifacts management required
  • TypeScript Integration Ready - Straightforward static typing implementation

Architectural Enhancements:

  • Islands Architecture Pattern - Progressive enhancement through isolated components
  • Authentic SSR + Hydration - Server renders initial content, client progressively enhances
  • Component Reusability - Universal components across environments
  • Maintainable Codebase - Elimination of template/logic separation complexity

Implementation Summary

The migration from Marko to Juris represents a transition to universal JavaScript applications featuring authentic server-side rendering and seamless client-side hydration. Primary benefits include:

  1. Universal Component Architecture: Identical code execution across server and client environments
  2. Build Pipeline Elimination: Direct execution in Node.js and browser environments
  3. True Islands Implementation: Progressive enhancement with isolated component functionality
  4. Performance Optimization: Optimized SSR with efficient state hydration
  5. Maintainability Enhancement: Pure JavaScript approach eliminates template complexity

This migration eliminates the maintenance overhead of separate template and JavaScript logic while delivering superior performance and developer experience. Juris's universal architecture provides server-side rendering benefits with client-side interactivity flexibility, all implemented in pure JavaScript without proprietary template syntax dependencies.

Top comments (0)