muspe-cli
Version:
MusPE Advanced Framework v2.1.3 - Mobile User-friendly Simple Progressive Engine with Enhanced CLI Tools, Specialized E-Commerce Templates, Material Design 3, Progressive Enhancement, Mobile Optimizations, Performance Analysis, and Enterprise-Grade Develo
1,030 lines (833 loc) • 24.5 kB
JavaScript
// MusPE Core Framework - MVC Architecture Implementation
import { MusPE } from './muspe.js';
// ============ MODEL LAYER ============
class MusPEModel extends MusPE {
constructor(data = {}, options = {}) {
super();
this.options = {
validateOnSet: true,
trackChanges: true,
enableUndo: false,
maxHistorySize: 50,
deepWatch: true,
computedProperties: {},
...options
};
// Model state
this._data = {};
this._computed = new Map();
this._validators = new Map();
this._relationships = new Map();
// Change tracking
this._changes = new Map();
this._history = [];
this._historyIndex = -1;
// Validation state
this._errors = new Map();
this._isValid = true;
// Events
this._changeListeners = new Map();
this._validationListeners = new Set();
this.init(data);
}
init(data) {
this.setupComputedProperties();
this.setData(data, false);
this.setupValidation();
}
// Data management
setData(data, notify = true) {
const oldData = { ...this._data };
Object.keys(data).forEach(key => {
this.set(key, data[key], false);
});
if (notify) {
this.emit('dataChanged', { oldData, newData: this._data });
}
}
get(key) {
if (this._computed.has(key)) {
return this._computed.get(key).call(this);
}
if (key.includes('.')) {
return this.getNestedValue(key);
}
return this._data[key];
}
set(key, value, notify = true) {
const oldValue = this.get(key);
// Validation
if (this.options.validateOnSet && !this.validateField(key, value)) {
return false;
}
// Set value
if (key.includes('.')) {
this.setNestedValue(key, value);
} else {
this._data[key] = value;
}
// Track changes
if (this.options.trackChanges) {
this._changes.set(key, { oldValue, newValue: value, timestamp: Date.now() });
}
// History
if (this.options.enableUndo) {
this.addToHistory(key, oldValue, value);
}
// Notify
if (notify) {
this.notifyChange(key, value, oldValue);
this.recomputeDependents(key);
}
return true;
}
getNestedValue(path) {
return path.split('.').reduce((obj, key) => obj?.[key], this._data);
}
setNestedValue(path, value) {
const keys = path.split('.');
const lastKey = keys.pop();
const target = keys.reduce((obj, key) => {
if (!obj[key] || typeof obj[key] !== 'object') {
obj[key] = {};
}
return obj[key];
}, this._data);
target[lastKey] = value;
}
// Computed properties
setupComputedProperties() {
Object.entries(this.options.computedProperties).forEach(([key, fn]) => {
this.addComputed(key, fn);
});
}
addComputed(key, fn, dependencies = []) {
this._computed.set(key, fn);
// Track dependencies for recomputation
dependencies.forEach(dep => {
if (!this._changeListeners.has(dep)) {
this._changeListeners.set(dep, new Set());
}
this._changeListeners.get(dep).add(() => {
this.recompute(key);
});
});
}
recompute(key) {
const computeFn = this._computed.get(key);
if (computeFn) {
const newValue = computeFn.call(this);
this.emit('computed', { key, value: newValue });
}
}
recomputeDependents(changedKey) {
const listeners = this._changeListeners.get(changedKey);
if (listeners) {
listeners.forEach(listener => listener());
}
}
// Validation
setupValidation() {
this.addValidator('required', (value) => value !== null && value !== undefined && value !== '');
this.addValidator('email', (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value));
this.addValidator('minLength', (value, min) => String(value).length >= min);
this.addValidator('maxLength', (value, max) => String(value).length <= max);
this.addValidator('numeric', (value) => !isNaN(Number(value)));
}
addValidator(name, validatorFn) {
this._validators.set(name, validatorFn);
}
validateField(key, value) {
const rules = this.getValidationRules(key);
if (!rules || rules.length === 0) return true;
const errors = [];
for (const rule of rules) {
const { validator, params, message } = this.parseValidationRule(rule);
const validatorFn = this._validators.get(validator);
if (validatorFn && !validatorFn(value, ...params)) {
errors.push(message || `Validation failed for ${key}`);
}
}
if (errors.length > 0) {
this._errors.set(key, errors);
this._isValid = false;
return false;
} else {
this._errors.delete(key);
this._isValid = this._errors.size === 0;
return true;
}
}
validateAll() {
let isValid = true;
Object.keys(this._data).forEach(key => {
if (!this.validateField(key, this.get(key))) {
isValid = false;
}
});
return isValid;
}
getValidationRules(key) {
return this.options.validationRules?.[key] || [];
}
parseValidationRule(rule) {
if (typeof rule === 'string') {
return { validator: rule, params: [], message: null };
}
return {
validator: rule.validator || rule.type,
params: rule.params || [],
message: rule.message
};
}
// Relationships
hasOne(key, modelClass, foreignKey = 'id') {
this._relationships.set(key, {
type: 'hasOne',
modelClass,
foreignKey
});
}
hasMany(key, modelClass, foreignKey) {
this._relationships.set(key, {
type: 'hasMany',
modelClass,
foreignKey
});
}
belongsTo(key, modelClass, foreignKey) {
this._relationships.set(key, {
type: 'belongsTo',
modelClass,
foreignKey
});
}
// History and undo/redo
addToHistory(key, oldValue, newValue) {
if (this._history.length >= this.options.maxHistorySize) {
this._history.shift();
}
this._history.push({
key,
oldValue,
newValue,
timestamp: Date.now()
});
this._historyIndex = this._history.length - 1;
}
undo() {
if (this._historyIndex > 0) {
const change = this._history[this._historyIndex];
this.set(change.key, change.oldValue, false);
this._historyIndex--;
this.emit('undo', change);
return true;
}
return false;
}
redo() {
if (this._historyIndex < this._history.length - 1) {
this._historyIndex++;
const change = this._history[this._historyIndex];
this.set(change.key, change.newValue, false);
this.emit('redo', change);
return true;
}
return false;
}
// Event handling
notifyChange(key, newValue, oldValue) {
this.emit('change', { key, newValue, oldValue });
this.emit(`change:${key}`, { newValue, oldValue });
}
// Serialization
toJSON() {
return { ...this._data };
}
fromJSON(json) {
this.setData(json);
}
// Public API
getData() {
return { ...this._data };
}
getErrors() {
return Object.fromEntries(this._errors);
}
isValid() {
return this._isValid;
}
getChanges() {
return Object.fromEntries(this._changes);
}
reset() {
this._data = {};
this._changes.clear();
this._errors.clear();
this._history = [];
this._historyIndex = -1;
this._isValid = true;
}
}
// ============ VIEW LAYER ============
class MusPEView extends MusPE {
constructor(options = {}) {
super();
this.options = {
template: '',
container: null,
autoRender: true,
enableVirtualDOM: true,
enableAnimations: true,
bindEvents: true,
...options
};
// View state
this.element = null;
this.template = this.options.template;
this.bindings = new Map();
this.eventBindings = new Map();
this.childViews = new Set();
// Virtual DOM
this.vdom = null;
this.previousVDOM = null;
// Rendering state
this.isRendered = false;
this.isDestroyed = false;
this.init();
}
init() {
this.setupTemplate();
this.createElement();
if (this.options.autoRender) {
this.render();
}
}
// Template management
setupTemplate() {
if (typeof this.template === 'function') {
// Template is a function
return;
}
if (this.template.startsWith('#')) {
// Template selector
const templateElement = document.querySelector(this.template);
this.template = templateElement?.innerHTML || '';
}
}
createElement() {
if (this.options.container) {
this.element = typeof this.options.container === 'string'
? document.querySelector(this.options.container)
: this.options.container;
} else {
this.element = document.createElement('div');
this.element.className = 'muspe-view';
}
}
// Rendering
render(data = {}) {
if (this.isDestroyed) return;
const renderData = { ...this.getData(), ...data };
if (this.options.enableVirtualDOM) {
this.renderWithVDOM(renderData);
} else {
this.renderDirect(renderData);
}
if (this.options.bindEvents) {
this.bindEvents();
}
this.isRendered = true;
this.emit('rendered', { data: renderData });
}
renderWithVDOM(data) {
const newVDOM = this.createVDOM(data);
if (this.previousVDOM) {
this.patchDOM(this.element, this.previousVDOM, newVDOM);
} else {
this.element.innerHTML = this.renderVDOM(newVDOM);
}
this.previousVDOM = newVDOM;
this.vdom = newVDOM;
}
renderDirect(data) {
const html = this.compileTemplate(data);
if (this.options.enableAnimations) {
this.animateRender(() => {
this.element.innerHTML = html;
});
} else {
this.element.innerHTML = html;
}
}
createVDOM(data) {
const template = typeof this.template === 'function'
? this.template(data)
: this.compileTemplate(data);
return this.parseHTML(template);
}
renderVDOM(vdom) {
if (typeof vdom === 'string') return vdom;
if (!vdom || !vdom.tag) return '';
const attrs = Object.entries(vdom.attrs || {})
.map(([key, value]) => `${key}="${value}"`)
.join(' ');
const children = (vdom.children || [])
.map(child => this.renderVDOM(child))
.join('');
return `<${vdom.tag}${attrs ? ' ' + attrs : ''}>${children}</${vdom.tag}>`;
}
compileTemplate(data) {
let html = this.template;
// Simple template interpolation
html = html.replace(/\{\{([^}]+)\}\}/g, (match, expression) => {
try {
const value = this.evaluateExpression(expression.trim(), data);
return value !== undefined ? String(value) : '';
} catch (error) {
console.warn('Template expression error:', error);
return match;
}
});
// Conditional rendering
html = html.replace(/\{\{#if\s+([^}]+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, condition, content) => {
try {
const result = this.evaluateExpression(condition.trim(), data);
return result ? content : '';
} catch (error) {
return '';
}
});
// Loop rendering
html = html.replace(/\{\{#each\s+([^}]+)\s+as\s+([^}]+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, arrayExpr, itemName, content) => {
try {
const array = this.evaluateExpression(arrayExpr.trim(), data);
if (!Array.isArray(array)) return '';
return array.map((item, index) => {
const itemData = { ...data, [itemName]: item, index };
return this.compileTemplate.call({ template: content }, itemData);
}).join('');
} catch (error) {
return '';
}
});
return html;
}
evaluateExpression(expression, data) {
// Simple expression evaluator
const func = new Function('data', `with(data) { return ${expression}; }`);
return func(data);
}
parseHTML(html) {
// Simple HTML parser for VDOM
const parser = new DOMParser();
const doc = parser.parseFromString(`<div>${html}</div>`, 'text/html');
return this.domToVDOM(doc.body.firstChild.firstChild);
}
domToVDOM(node) {
if (node.nodeType === Node.TEXT_NODE) {
return node.textContent;
}
if (node.nodeType === Node.ELEMENT_NODE) {
const attrs = {};
Array.from(node.attributes).forEach(attr => {
attrs[attr.name] = attr.value;
});
const children = Array.from(node.childNodes).map(child => this.domToVDOM(child));
return {
tag: node.tagName.toLowerCase(),
attrs,
children
};
}
return null;
}
patchDOM(parent, oldVDOM, newVDOM) {
// Simple VDOM diffing and patching
if (!oldVDOM) {
parent.appendChild(this.createElementFromVDOM(newVDOM));
} else if (!newVDOM) {
parent.removeChild(parent.firstChild);
} else if (this.isVDOMDifferent(oldVDOM, newVDOM)) {
parent.replaceChild(
this.createElementFromVDOM(newVDOM),
parent.firstChild
);
} else if (newVDOM.tag) {
const element = parent.firstChild;
// Update attributes
this.updateAttributes(element, oldVDOM.attrs || {}, newVDOM.attrs || {});
// Update children
const oldChildren = oldVDOM.children || [];
const newChildren = newVDOM.children || [];
for (let i = 0; i < Math.max(oldChildren.length, newChildren.length); i++) {
this.patchDOM(element, oldChildren[i], newChildren[i]);
}
}
}
createElementFromVDOM(vdom) {
if (typeof vdom === 'string') {
return document.createTextNode(vdom);
}
const element = document.createElement(vdom.tag);
Object.entries(vdom.attrs || {}).forEach(([key, value]) => {
element.setAttribute(key, value);
});
(vdom.children || []).forEach(child => {
element.appendChild(this.createElementFromVDOM(child));
});
return element;
}
isVDOMDifferent(oldVDOM, newVDOM) {
if (typeof oldVDOM !== typeof newVDOM) return true;
if (typeof oldVDOM === 'string') return oldVDOM !== newVDOM;
if (oldVDOM.tag !== newVDOM.tag) return true;
return false;
}
updateAttributes(element, oldAttrs, newAttrs) {
// Remove old attributes
Object.keys(oldAttrs).forEach(key => {
if (!(key in newAttrs)) {
element.removeAttribute(key);
}
});
// Set new attributes
Object.entries(newAttrs).forEach(([key, value]) => {
if (oldAttrs[key] !== value) {
element.setAttribute(key, value);
}
});
}
// Event binding
bindEvents() {
if (!this.element) return;
// Clear previous bindings
this.unbindEvents();
// Bind new events
this.element.querySelectorAll('[data-event]').forEach(element => {
const eventConfig = element.dataset.event;
const [eventName, methodName] = eventConfig.split(':');
if (typeof this[methodName] === 'function') {
const handler = this[methodName].bind(this);
element.addEventListener(eventName, handler);
if (!this.eventBindings.has(element)) {
this.eventBindings.set(element, new Map());
}
this.eventBindings.get(element).set(eventName, handler);
}
});
}
unbindEvents() {
this.eventBindings.forEach((events, element) => {
events.forEach((handler, eventName) => {
element.removeEventListener(eventName, handler);
});
});
this.eventBindings.clear();
}
// Animation helpers
animateRender(renderFn) {
this.element.style.opacity = '0';
this.element.style.transform = 'translateY(10px)';
renderFn();
requestAnimationFrame(() => {
this.element.style.transition = 'all 0.3s ease';
this.element.style.opacity = '1';
this.element.style.transform = 'translateY(0)';
});
}
// Child view management
addChildView(view) {
this.childViews.add(view);
view.parentView = this;
}
removeChildView(view) {
this.childViews.delete(view);
view.parentView = null;
}
// Lifecycle
show() {
if (this.element) {
this.element.style.display = '';
this.emit('shown');
}
}
hide() {
if (this.element) {
this.element.style.display = 'none';
this.emit('hidden');
}
}
destroy() {
this.unbindEvents();
this.childViews.forEach(child => child.destroy());
this.childViews.clear();
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.isDestroyed = true;
this.emit('destroyed');
super.destroy();
}
// Data binding (to be overridden by subclasses)
getData() {
return {};
}
}
// ============ CONTROLLER LAYER ============
class MusPEController extends MusPE {
constructor(options = {}) {
super();
this.options = {
autoInitialize: true,
enableRouting: false,
enableCaching: false,
...options
};
// MVC components
this.model = null;
this.view = null;
this.services = new Map();
// Controller state
this.isInitialized = false;
this.isDestroyed = false;
// Action handlers
this.actions = new Map();
this.middlewares = [];
// Caching
this.cache = this.options.enableCaching ? new Map() : null;
if (this.options.autoInitialize) {
this.initialize();
}
}
initialize() {
if (this.isInitialized) return;
this.setupModel();
this.setupView();
this.setupServices();
this.bindModelToView();
this.registerActions();
this.isInitialized = true;
this.emit('initialized');
}
// MVC setup
setupModel() {
// Override in subclasses
}
setupView() {
// Override in subclasses
}
setupServices() {
// Override in subclasses
}
bindModelToView() {
if (!this.model || !this.view) return;
// Bind model changes to view updates
this.model.on('change', (data) => {
this.view.render(this.model.getData());
this.emit('modelChanged', data);
});
// Bind view events to controller actions
this.view.on('*', (eventName, data) => {
this.handleViewEvent(eventName, data);
});
}
handleViewEvent(eventName, data) {
const actionName = `on${eventName.charAt(0).toUpperCase()}${eventName.slice(1)}`;
if (typeof this[actionName] === 'function') {
this.executeAction(actionName, data);
}
}
// Action system
registerAction(name, handler, middleware = []) {
this.actions.set(name, {
handler,
middleware: [...this.middlewares, ...middleware]
});
}
async executeAction(name, ...args) {
const action = this.actions.get(name);
if (!action) {
console.warn(`Action '${name}' not found`);
return;
}
try {
// Execute middlewares
for (const middleware of action.middleware) {
const result = await middleware(name, args, this);
if (result === false) {
console.log(`Action '${name}' blocked by middleware`);
return;
}
}
// Execute action
const result = await action.handler.apply(this, args);
this.emit('actionExecuted', { name, args, result });
return result;
} catch (error) {
console.error(`Action '${name}' failed:`, error);
this.emit('actionError', { name, args, error });
throw error;
}
}
addMiddleware(middleware) {
this.middlewares.push(middleware);
}
// Service management
addService(name, service) {
this.services.set(name, service);
// Auto-inject into model and view
if (this.model && typeof this.model.setService === 'function') {
this.model.setService(name, service);
}
if (this.view && typeof this.view.setService === 'function') {
this.view.setService(name, service);
}
}
getService(name) {
return this.services.get(name);
}
// Data operations
async loadData(params = {}) {
const cacheKey = JSON.stringify(params);
if (this.cache && this.cache.has(cacheKey)) {
const data = this.cache.get(cacheKey);
this.updateModel(data);
return data;
}
try {
const data = await this.fetchData(params);
if (this.cache) {
this.cache.set(cacheKey, data);
}
this.updateModel(data);
this.emit('dataLoaded', { params, data });
return data;
} catch (error) {
console.error('Failed to load data:', error);
this.emit('dataError', { params, error });
throw error;
}
}
async saveData(data = null) {
const saveData = data || (this.model ? this.model.getData() : {});
if (this.model && !this.model.isValid()) {
const errors = this.model.getErrors();
this.emit('validationError', { errors });
throw new Error('Validation failed');
}
try {
const result = await this.persistData(saveData);
this.emit('dataSaved', { data: saveData, result });
return result;
} catch (error) {
console.error('Failed to save data:', error);
this.emit('saveError', { data: saveData, error });
throw error;
}
}
updateModel(data) {
if (this.model) {
if (typeof this.model.setData === 'function') {
this.model.setData(data);
} else {
Object.assign(this.model, data);
}
}
}
// Data layer methods (to be overridden)
async fetchData(params) {
throw new Error('fetchData method must be implemented');
}
async persistData(data) {
throw new Error('persistData method must be implemented');
}
// Navigation and routing
navigate(route, params = {}) {
if (this.options.enableRouting && window.MusPERouter) {
window.MusPERouter.navigate(route, params);
} else {
console.warn('Routing not enabled or router not available');
}
}
// Lifecycle
refresh() {
if (this.view) {
this.view.render(this.model ? this.model.getData() : {});
}
}
reset() {
if (this.model && typeof this.model.reset === 'function') {
this.model.reset();
}
if (this.cache) {
this.cache.clear();
}
this.refresh();
this.emit('reset');
}
destroy() {
if (this.model && typeof this.model.destroy === 'function') {
this.model.destroy();
}
if (this.view && typeof this.view.destroy === 'function') {
this.view.destroy();
}
this.services.forEach(service => {
if (typeof service.destroy === 'function') {
service.destroy();
}
});
this.isDestroyed = true;
this.emit('destroyed');
super.destroy();
}
}
// ============ MVC FACTORY ============
class MusPEMVCFactory {
static createMVC(options = {}) {
const {
modelClass = MusPEModel,
viewClass = MusPEView,
controllerClass = MusPEController,
modelOptions = {},
viewOptions = {},
controllerOptions = {}
} = options;
const model = new modelClass(modelOptions);
const view = new viewClass(viewOptions);
const controller = new controllerClass({
...controllerOptions,
autoInitialize: false
});
// Inject dependencies
controller.model = model;
controller.view = view;
// Initialize
controller.initialize();
return { model, view, controller };
}
static createModel(data, options) {
return new MusPEModel(data, options);
}
static createView(options) {
return new MusPEView(options);
}
static createController(options) {
return new MusPEController(options);
}
}
// Export classes
export {
MusPEModel,
MusPEView,
MusPEController,
MusPEMVCFactory
};
// Global registration
if (typeof window !== 'undefined') {
window.MusPEModel = MusPEModel;
window.MusPEView = MusPEView;
window.MusPEController = MusPEController;
window.MusPEMVCFactory = MusPEMVCFactory;
}