UNPKG

veko

Version:

Ultra-lightweight Node.js framework with ZERO dependencies - VSV components, Tailwind CSS, asset imports, SSR

1,296 lines (1,170 loc) 44 kB
/** * VSV Runtime - Complete React-equivalent for .jsv/.tsv * All React features reimplemented with zero dependencies * Client-side runtime for hydration, reactivity, routing, etc. */ class VSVRuntime { constructor(vsv) { this.vsv = vsv; } getClientRuntime() { return `/** * VSV Client Runtime v2.0 * Complete React-equivalent - Zero Dependencies * Supports: Hooks, Context, Router, Suspense, Portals, ErrorBoundary, CSS-in-JS, Forms */ (function(global) { 'use strict'; if (global.VSV) return; // UID generator let _uid = 0; function uid() { return '_vsv_' + (++_uid); } // ======================== // VIRTUAL DOM // ======================== const Fragment = Symbol('Fragment'); const Portal = Symbol('Portal'); function h(tag, props) { var children = []; for (var i = 2; i < arguments.length; i++) { var c = arguments[i]; if (Array.isArray(c)) children = children.concat(c); else if (c != null && c !== false && c !== true) children.push(c); } return { tag: tag, props: props || {}, children: children, key: props && props.key }; } // ======================== // HOOKS SYSTEM // ======================== var currentComponent = null; var hookIndex = 0; function getHook() { if (!currentComponent) throw new Error('Hooks can only be used inside components'); if (!currentComponent._hooks) currentComponent._hooks = []; var idx = hookIndex++; return { component: currentComponent, index: idx }; } // --- $state (useState) --- function $state(initial) { var h = getHook(); var comp = h.component; if (comp._hooks[h.index] === undefined) { comp._hooks[h.index] = { value: typeof initial === 'function' ? initial() : initial }; } var hook = comp._hooks[h.index]; var getter = function() { return hook.value; }; var setter = function(v) { var next = typeof v === 'function' ? v(hook.value) : v; if (next !== hook.value) { hook.value = next; scheduleRerender(comp); } }; return [getter, setter]; } // --- $effect (useEffect) --- function $effect(fn, deps) { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = { deps: undefined, cleanup: null, fn: fn }; } var hook = comp._hooks[h.index]; hook.fn = fn; var prevDeps = hook.deps; var changed = !prevDeps || !deps || deps.length !== prevDeps.length || deps.some(function(d, i) { return d !== prevDeps[i]; }); if (changed) { hook.deps = deps ? deps.slice() : undefined; queueEffect(function() { if (hook.cleanup) hook.cleanup(); hook.cleanup = fn() || null; }); } } // --- $computed (useMemo with getter) --- function $computed(fn, deps) { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = { value: undefined, deps: undefined }; } var hook = comp._hooks[h.index]; var prevDeps = hook.deps; var changed = !prevDeps || !deps || deps.length !== prevDeps.length || deps.some(function(d, i) { return d !== prevDeps[i]; }); if (changed || hook.deps === undefined) { hook.value = fn(); hook.deps = deps ? deps.slice() : undefined; } return function() { return hook.value; }; } // --- $ref (useRef) --- function $ref(initial) { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = { current: initial !== undefined ? initial : null }; } return comp._hooks[h.index]; } // --- $memo (useMemo) --- function $memo(fn, deps) { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = { value: undefined, deps: undefined }; } var hook = comp._hooks[h.index]; var prevDeps = hook.deps; var changed = !prevDeps || !deps || deps.length !== prevDeps.length || deps.some(function(d, i) { return d !== prevDeps[i]; }); if (changed || hook.deps === undefined) { hook.value = fn(); hook.deps = deps ? deps.slice() : undefined; } return hook.value; } // --- $callback (useCallback) --- function $callback(fn, deps) { return $memo(function() { return fn; }, deps); } // --- $reducer (useReducer) --- function $reducer(reducer, initialState, init) { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = { state: init ? init(initialState) : initialState }; } var hook = comp._hooks[h.index]; var dispatch = function(action) { var next = reducer(hook.state, action); if (next !== hook.state) { hook.state = next; scheduleRerender(comp); } }; return [hook.state, dispatch]; } // --- $id (useId) --- function $id() { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = uid(); } return comp._hooks[h.index]; } // --- $transition (useTransition) --- function $transition() { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = { pending: false }; } var hook = comp._hooks[h.index]; var startTransition = function(fn) { hook.pending = true; scheduleRerender(comp); requestIdleCallback(function() { fn(); hook.pending = false; scheduleRerender(comp); }); }; return [hook.pending, startTransition]; } // --- $deferred (useDeferredValue) --- function $deferred(value, options) { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = { value: value }; } var hook = comp._hooks[h.index]; if (hook.value !== value) { if (hook.timer) clearTimeout(hook.timer); hook.timer = setTimeout(function() { hook.value = value; scheduleRerender(comp); }, (options && options.timeoutMs) || 200); } return hook.value; } // --- $context (useContext) --- function $context(ctx) { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = {}; } // Walk up to find provider var node = comp; while (node) { if (node._contextValues && node._contextValues.has(ctx)) { return node._contextValues.get(ctx); } node = node._parent; } return ctx._defaultValue; } // --- $layoutEffect (useLayoutEffect) --- function $layoutEffect(fn, deps) { var h = getHook(); var comp = h.component; if (!comp._hooks[h.index]) { comp._hooks[h.index] = { deps: undefined, cleanup: null }; } var hook = comp._hooks[h.index]; var prevDeps = hook.deps; var changed = !prevDeps || !deps || deps.length !== prevDeps.length || deps.some(function(d, i) { return d !== prevDeps[i]; }); if (changed) { hook.deps = deps ? deps.slice() : undefined; // Layout effects run synchronously after DOM mutations if (hook.cleanup) hook.cleanup(); hook.cleanup = fn() || null; } } // --- $imperativeHandle (useImperativeHandle) --- function $imperativeHandle(ref, create, deps) { $layoutEffect(function() { var value = create(); if (typeof ref === 'function') ref(value); else if (ref) ref.current = value; }, deps); } // --- $debugValue (useDebugValue) --- function $debugValue(value, formatter) { // Only used in dev tools - no-op in production } // --- $syncExternalStore (useSyncExternalStore) --- function $syncExternalStore(subscribe, getSnapshot) { var _state = $state(getSnapshot); var value = _state[0]; var setValue = _state[1]; $effect(function() { return subscribe(function() { setValue(getSnapshot()); }); }, [subscribe, getSnapshot]); return value(); } // ======================== // CONTEXT API // ======================== function createContext(defaultValue) { var ctx = { _defaultValue: defaultValue, _currentValue: defaultValue, Provider: function(props) { ctx._currentValue = props.value !== undefined ? props.value : defaultValue; // Mark component context if (currentComponent) { if (!currentComponent._contextValues) currentComponent._contextValues = new Map(); currentComponent._contextValues.set(ctx, ctx._currentValue); } return props.children; }, Consumer: function(props) { var value = ctx._currentValue; return typeof props.children === 'function' ? props.children(value) : props.children; } }; return ctx; } // ======================== // ROUTER (SPA) // ======================== var routerListeners = []; var currentPath = typeof window !== 'undefined' ? window.location.pathname : '/'; var routerParams = {}; var routerQuery = {}; function RouterContext() { return { path: currentPath, params: routerParams, query: routerQuery }; } function navigate(to, options) { if (typeof window === 'undefined') return; if (options && options.replace) { window.history.replaceState({}, '', to); } else { window.history.pushState({}, '', to); } currentPath = to; routerQuery = parseQuery(window.location.search); notifyRouterListeners(); } function parseQuery(search) { var q = {}; if (!search || search.length < 2) return q; search.slice(1).split('&').forEach(function(p) { var kv = p.split('='); q[decodeURIComponent(kv[0])] = decodeURIComponent(kv[1] || ''); }); return q; } function notifyRouterListeners() { routerListeners.forEach(function(fn) { fn(currentPath); }); } if (typeof window !== 'undefined') { window.addEventListener('popstate', function() { currentPath = window.location.pathname; routerQuery = parseQuery(window.location.search); notifyRouterListeners(); }); } // Router component function Router(props) { var _state = $state(currentPath); var path = _state[0]; var setPath = _state[1]; $effect(function() { var listener = function(p) { setPath(p); }; routerListeners.push(listener); return function() { routerListeners = routerListeners.filter(function(l) { return l !== listener; }); }; }, []); var currentRoute = path(); var routes = Array.isArray(props.children) ? props.children : [props.children]; var matched = null; var fallback = null; for (var i = 0; i < routes.length; i++) { var route = routes[i]; if (!route || !route.props) continue; if (route.props.path === '*') { fallback = route; continue; } var m = matchRoute(route.props.path, currentRoute); if (m) { routerParams = m; matched = route; break; } } if (matched && matched.props.component) { return h(matched.props.component, Object.assign({}, matched.props, { params: routerParams, query: routerQuery })); } if (fallback && fallback.props.component) { return h(fallback.props.component, { params: {}, query: routerQuery }); } return null; } function Route(props) { return null; } function matchRoute(pattern, pathname) { if (!pattern) return null; var keys = []; var re = pattern.replace(/:([\\w]+)/g, function(_, k) { keys.push(k); return '([^/]+)'; }); var m = pathname.match(new RegExp('^' + re + '/?$')); if (!m) return null; var params = {}; keys.forEach(function(k, i) { params[k] = m[i + 1]; }); return params; } // Link component function Link(props) { return h('a', { href: props.to || props.href, className: props.className || props.class, style: props.style, onClick: function(e) { if (!e.ctrlKey && !e.metaKey && !e.shiftKey && !props.external) { e.preventDefault(); navigate(props.to || props.href, props); } if (props.onClick) props.onClick(e); } }, props.children); } // NavLink - active-aware Link function NavLink(props) { var isActive = currentPath === (props.to || props.href); var cls = props.className || ''; if (isActive && props.activeClass) cls += ' ' + props.activeClass; return h(Link, Object.assign({}, props, { className: cls.trim() })); } // Redirect component function Redirect(props) { $effect(function() { navigate(props.to, { replace: true }); }, []); return null; } // Router hooks function $location() { var _s = $state(currentPath); var path = _s[0]; var setPath = _s[1]; $effect(function() { var fn = function(p) { setPath(p); }; routerListeners.push(fn); return function() { routerListeners = routerListeners.filter(function(l) { return l !== fn; }); }; }, []); return { pathname: path(), params: routerParams, query: routerQuery, navigate: navigate }; } function $params() { return routerParams; } function $query() { return routerQuery; } function $navigate() { return navigate; } // ======================== // SUSPENSE & LAZY // ======================== function Suspense(props) { try { return props.children; } catch (promise) { if (promise && typeof promise.then === 'function') { var _s = $state(false); var resolved = _s[0]; var setResolved = _s[1]; promise.then(function() { setResolved(true); }); return resolved() ? props.children : (props.fallback || h('div', null, 'Loading...')); } throw promise; } } function lazy(loader) { var Component = null; var promise = null; var error = null; return function LazyComponent(props) { if (error) throw error; if (Component) return h(Component, props); if (!promise) { promise = loader().then(function(mod) { Component = mod.default || mod; }).catch(function(e) { error = e; }); } throw promise; }; } // ======================== // ERROR BOUNDARY // ======================== function ErrorBoundary(props) { var _s = $state(null); var error = _s[0]; var setError = _s[1]; if (error()) { if (props.fallback) { return typeof props.fallback === 'function' ? props.fallback({ error: error(), reset: function() { setError(null); } }) : props.fallback; } return h('div', { style: 'color:red;padding:20px;' }, 'Error: ' + error().message); } try { return props.children; } catch (e) { setError(e); return null; } } // ======================== // PORTAL // ======================== function createPortal(children, container) { return { tag: Portal, props: { container: container }, children: Array.isArray(children) ? children : [children] }; } // ======================== // FORWARD REF // ======================== function forwardRef(renderFn) { return function ForwardRefComponent(props) { return renderFn(props, props.ref); }; } // ======================== // CHILDREN API // ======================== var Children = { map: function(children, fn) { if (!children) return []; var arr = Array.isArray(children) ? children : [children]; return arr.filter(function(c) { return c != null; }).map(fn); }, forEach: function(children, fn) { Children.map(children, fn); }, count: function(children) { if (!children) return 0; return Array.isArray(children) ? children.filter(function(c) { return c != null; }).length : 1; }, toArray: function(children) { if (!children) return []; return Array.isArray(children) ? children.filter(function(c) { return c != null; }) : [children]; }, only: function(children) { if (Children.count(children) !== 1) throw new Error('Children.only expects exactly one child'); return Array.isArray(children) ? children[0] : children; } }; // ======================== // CONDITIONAL HELPERS // ======================== // <Show when={condition}> ... </Show> function Show(props) { return props.when ? props.children : (props.fallback || null); } // <For each={items}>{(item, i) => <div>{item}</div>}</For> function For(props) { var items = props.each || []; var fn = props.children; if (typeof fn !== 'function') return null; return items.map(function(item, i) { return fn(item, i); }); } // <Switch> <Match when={...}>...</Match> </Switch> function Switch(props) { var children = Array.isArray(props.children) ? props.children : [props.children]; for (var i = 0; i < children.length; i++) { if (children[i] && children[i].props && children[i].props.when) { return children[i].props.children || children[i]; } } return props.fallback || null; } function Match(props) { return props.children; } // ======================== // HEAD MANAGEMENT // ======================== function Head(props) { $effect(function() { // Update document title var children = Array.isArray(props.children) ? props.children : [props.children]; children.forEach(function(child) { if (!child || !child.tag) return; if (child.tag === 'title') { document.title = child.children.join(''); } else if (child.tag === 'meta') { var existing = document.querySelector('meta[name="' + child.props.name + '"]') || document.querySelector('meta[property="' + child.props.property + '"]'); if (existing) { existing.setAttribute('content', child.props.content || ''); } else { var meta = document.createElement('meta'); Object.keys(child.props).forEach(function(k) { meta.setAttribute(k, child.props[k]); }); document.head.appendChild(meta); } } else if (child.tag === 'link') { var link = document.createElement('link'); Object.keys(child.props).forEach(function(k) { link.setAttribute(k, child.props[k]); }); document.head.appendChild(link); } }); }, [props.children]); return null; } // ======================== // CSS-IN-JS // ======================== var styleSheet = null; var styleCache = new Map(); function css(strings) { var args = []; for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); var raw = strings.reduce(function(acc, str, j) { return acc + str + (args[j] || ''); }, ''); if (styleCache.has(raw)) return styleCache.get(raw); var className = 'vsv-' + hashStr(raw); if (typeof document !== 'undefined') { if (!styleSheet) { var style = document.createElement('style'); style.id = 'vsv-styles'; document.head.appendChild(style); styleSheet = style.sheet; } try { styleSheet.insertRule('.' + className + '{' + raw + '}', styleSheet.cssRules.length); } catch(e) {} } styleCache.set(raw, className); return className; } function styled(tag) { return function(strings) { var args = []; for (var i = 1; i < arguments.length; i++) args.push(arguments[i]); return function StyledComponent(props) { var resolved = strings.reduce(function(acc, str, j) { var arg = args[j]; return acc + str + (typeof arg === 'function' ? arg(props) : (arg || '')); }, ''); var className = css([resolved]); var newProps = Object.assign({}, props, { className: (props.className || '') + ' ' + className }); delete newProps.children; return h(tag, newProps, props.children); }; }; } // ======================== // FORM HANDLING // ======================== function $form(config) { var initialValues = config.initialValues || {}; var _values = $state(Object.assign({}, initialValues)); var values = _values[0]; var setValues = _values[1]; var _errors = $state({}); var errors = _errors[0]; var setErrors = _errors[1]; var _touched = $state({}); var touched = _touched[0]; var setTouched = _touched[1]; var _submitting = $state(false); var submitting = _submitting[0]; var setSubmitting = _submitting[1]; return { values: values, errors: errors, touched: touched, submitting: submitting, handleChange: function(e) { var name = e.target.name; var value = e.target.type === 'checkbox' ? e.target.checked : e.target.value; setValues(function(prev) { var next = Object.assign({}, prev); next[name] = value; return next; }); setTouched(function(prev) { var next = Object.assign({}, prev); next[name] = true; return next; }); if (config.validate) { var errs = config.validate(Object.assign({}, values(), ( function() { var o = {}; o[name] = value; return o; } )())); setErrors(errs || {}); } }, handleBlur: function(e) { var name = e.target.name; setTouched(function(prev) { var next = Object.assign({}, prev); next[name] = true; return next; }); }, handleSubmit: function(e) { if (e && e.preventDefault) e.preventDefault(); if (config.validate) { var errs = config.validate(values()); setErrors(errs || {}); if (Object.keys(errs || {}).length > 0) return; } setSubmitting(true); var result = config.onSubmit ? config.onSubmit(values()) : null; if (result && typeof result.then === 'function') { result.then(function() { setSubmitting(false); }).catch(function() { setSubmitting(false); }); } else { setSubmitting(false); } }, setFieldValue: function(name, value) { setValues(function(prev) { var next = Object.assign({}, prev); next[name] = value; return next; }); }, setFieldError: function(name, error) { setErrors(function(prev) { var next = Object.assign({}, prev); next[name] = error; return next; }); }, reset: function() { setValues(Object.assign({}, initialValues)); setErrors({}); setTouched({}); setSubmitting(false); }, isValid: function() { return Object.keys(errors()).length === 0; } }; } // ======================== // FETCH / API HOOK // ======================== function $fetch(urlOrFn, options) { var _data = $state(null); var data = _data[0]; var setData = _data[1]; var _loading = $state(true); var loading = _loading[0]; var setLoading = _loading[1]; var _error = $state(null); var error = _error[0]; var setError = _error[1]; var fetchData = function() { setLoading(true); setError(null); var url = typeof urlOrFn === 'function' ? urlOrFn() : urlOrFn; fetch(url, options) .then(function(res) { return res.json(); }) .then(function(d) { setData(d); setLoading(false); }) .catch(function(e) { setError(e); setLoading(false); }); }; $effect(fetchData, [typeof urlOrFn === 'function' ? urlOrFn() : urlOrFn]); return { data: data, loading: loading, error: error, refetch: fetchData }; } // ======================== // STORE (like Redux/Zustand) // ======================== function createStore(initialState) { var state = Object.assign({}, initialState); var listeners = []; return { getState: function() { return state; }, setState: function(partial) { state = Object.assign({}, state, typeof partial === 'function' ? partial(state) : partial); listeners.forEach(function(fn) { fn(state); }); }, subscribe: function(fn) { listeners.push(fn); return function() { listeners = listeners.filter(function(l) { return l !== fn; }); }; }, use: function(selector) { var sel = selector || function(s) { return s; }; return $syncExternalStore( function(cb) { return listeners.push(cb) && function() { listeners = listeners.filter(function(l) { return l !== cb; }); }; }, function() { return sel(state); } ); } }; } // ======================== // ANIMATION // ======================== function $animation(config) { var _ref = $ref(null); var _running = $state(false); var running = _running[0]; var setRunning = _running[1]; var start = function() { if (!_ref.current) return; setRunning(true); var el = _ref.current; var from = config.from || {}; var to = config.to || {}; var duration = config.duration || 300; var startTime = performance.now(); Object.keys(from).forEach(function(k) { el.style[k] = from[k]; }); function tick(now) { var progress = Math.min((now - startTime) / duration, 1); var eased = config.easing ? config.easing(progress) : progress; Object.keys(to).forEach(function(k) { var fromVal = parseFloat(from[k]) || 0; var toVal = parseFloat(to[k]) || 0; var unit = (to[k] + '').replace(/[0-9.-]/g, '') || ''; el.style[k] = (fromVal + (toVal - fromVal) * eased) + unit; }); if (progress < 1) requestAnimationFrame(tick); else { setRunning(false); if (config.onComplete) config.onComplete(); } } requestAnimationFrame(tick); }; return { ref: _ref, start: start, running: running }; } // ======================== // DOM CREATION // ======================== function createElement(vnode) { if (vnode == null || vnode === false || vnode === true) return document.createTextNode(''); if (typeof vnode === 'string' || typeof vnode === 'number') return document.createTextNode(String(vnode)); if (Array.isArray(vnode)) { var frag = document.createDocumentFragment(); vnode.forEach(function(v) { frag.appendChild(createElement(v)); }); return frag; } var tag = vnode.tag, props = vnode.props || {}, children = vnode.children || []; // Fragment if (tag === Fragment) { var frag = document.createDocumentFragment(); children.forEach(function(c) { frag.appendChild(createElement(c)); }); return frag; } // Portal if (tag === Portal) { var container = typeof props.container === 'string' ? document.querySelector(props.container) : props.container; if (container) children.forEach(function(c) { container.appendChild(createElement(c)); }); return document.createTextNode(''); } // Component if (typeof tag === 'function') { var resolvedProps = resolveProps(tag, Object.assign({}, props, { children: children })); var comp = { _hooks: [], _parent: currentComponent, _contextValues: null, render: tag, props: resolvedProps }; var prev = currentComponent; currentComponent = comp; hookIndex = 0; var result = tag(resolvedProps); currentComponent = prev; return createElement(result); } // Element var el = document.createElement(tag); setProps(el, props); children.forEach(function(c) { el.appendChild(createElement(c)); }); return el; } function setProps(el, props) { for (var key in props) { if (key === 'children' || key === 'key') continue; var value = props[key]; if (key.startsWith('on') && typeof value === 'function') { el.addEventListener(key.slice(2).toLowerCase(), value); } else if (key === 'className' || key === 'class') { el.className = value; } else if (key === 'style') { if (typeof value === 'object') Object.assign(el.style, value); else el.style.cssText = value; } else if (key === 'ref') { if (typeof value === 'function') value(el); else if (value && typeof value === 'object') value.current = el; } else if (key === 'dangerouslySetInnerHTML') { el.innerHTML = (value && value.__html) || ''; } else if (value === true) { el.setAttribute(key, ''); } else if (value !== false && value != null) { el.setAttribute(key, String(value)); } } } // ======================== // PATCHING (RECONCILER) // ======================== function patch(dom, oldVnode, newVnode) { if (typeof oldVnode !== typeof newVnode) { var n = createElement(newVnode); if (dom.parentNode) dom.parentNode.replaceChild(n, dom); return n; } if (typeof newVnode === 'string' || typeof newVnode === 'number') { if (String(oldVnode) !== String(newVnode)) dom.textContent = String(newVnode); return dom; } if (newVnode == null) { if (dom.parentNode) dom.remove(); return null; } if (oldVnode && oldVnode.tag !== newVnode.tag) { var n = createElement(newVnode); if (dom.parentNode) dom.parentNode.replaceChild(n, dom); return n; } patchProps(dom, (oldVnode && oldVnode.props) || {}, newVnode.props || {}); patchChildren(dom, (oldVnode && oldVnode.children) || [], newVnode.children || []); return dom; } function patchProps(dom, oldP, newP) { for (var k in oldP) { if (!(k in newP)) { if (k === 'className') dom.className = ''; else if (!k.startsWith('on')) dom.removeAttribute(k); } } for (var k in newP) { if (oldP[k] !== newP[k]) { var v = newP[k]; if (k.startsWith('on') && typeof v === 'function') { var ev = k.slice(2).toLowerCase(); if (oldP[k]) dom.removeEventListener(ev, oldP[k]); dom.addEventListener(ev, v); } else if (k === 'className' || k === 'class') { dom.className = v; } else if (k === 'style' && typeof v === 'object') { Object.assign(dom.style, v); } else if (v === true) { dom.setAttribute(k, ''); } else if (v === false || v == null) { dom.removeAttribute(k); } else { dom.setAttribute(k, String(v)); } } } } function patchChildren(dom, oldC, newC) { var max = Math.max(oldC.length, newC.length); for (var i = 0; i < max; i++) { if (i >= oldC.length) { dom.appendChild(createElement(newC[i])); } else if (i >= newC.length) { if (dom.childNodes[i]) dom.childNodes[i].remove(); } else { patch(dom.childNodes[i], oldC[i], newC[i]); } } } // ======================== // RENDER / HYDRATE // ======================== var effects = []; var rerenderQueue = new Set(); var batchScheduled = false; function queueEffect(fn) { effects.push(fn); } function flushEffects() { var pending = effects.splice(0); pending.forEach(function(fn) { fn(); }); } function scheduleRerender(comp) { rerenderQueue.add(comp); if (!batchScheduled) { batchScheduled = true; queueMicrotask(function() { batchScheduled = false; rerenderQueue.forEach(function(c) { if (c._dom && c.render) { var prev = currentComponent; currentComponent = c; hookIndex = 0; var newVnode = c.render(c.props); patch(c._dom, c._vnode, newVnode); c._vnode = newVnode; currentComponent = prev; } }); rerenderQueue.clear(); flushEffects(); }); } } function render(vnode, container) { if (typeof container === 'string') container = document.querySelector(container); container.innerHTML = ''; container.appendChild(createElement(vnode)); flushEffects(); } function hydrate() { var root = document.getElementById('app'); if (!root) return; var markers = root.querySelectorAll('[data-vsv]'); markers.forEach(function(node) { var name = node.dataset.vsv; var propsStr = node.dataset.vsvProps; var props = {}; try { props = propsStr ? JSON.parse(propsStr.replace(/&quot;/g, '"')) : {}; } catch(e) {} var Component = VSV.components.get(name); if (!Component) return; var comp = { _hooks: [], _dom: node, _vnode: null, render: Component, props: props, _parent: null }; var prev = currentComponent; currentComponent = comp; hookIndex = 0; comp._vnode = Component(props); currentComponent = prev; flushEffects(); VSV.instances.set(node, comp); }); } // ======================== // UTILITIES // ======================== function hashStr(s) { var h = 0; for (var i = 0; i < s.length; i++) h = ((h << 5) - h + s.charCodeAt(i)) | 0; return Math.abs(h).toString(36); } function cloneElement(element, newProps) { return h(element.tag, Object.assign({}, element.props, newProps), element.children); } function isValidElement(obj) { return obj != null && typeof obj === 'object' && 'tag' in obj; } function memo(Component, compare) { var lastProps = null; var lastResult = null; return function MemoComponent(props) { var eq = compare ? compare(lastProps, props) : shallowEqual(lastProps, props); if (eq && lastResult) return lastResult; lastProps = props; lastResult = Component(props); return lastResult; }; } function shallowEqual(a, b) { if (a === b) return true; if (!a || !b) return false; var keysA = Object.keys(a); var keysB = Object.keys(b); if (keysA.length !== keysB.length) return false; return keysA.every(function(k) { return a[k] === b[k]; }); } // ======================== // PROP TYPES // ======================== function createChecker(check, typeName) { var fn = function(props, propName, componentName) { var value = props[propName]; if (value == null) return fn.isRequired ? new Error(componentName + ': prop "' + propName + '" is required') : null; return check(value, propName, componentName); }; fn.isRequired = false; fn.typeName = typeName; var required = function(props, propName, componentName) { if (props[propName] == null) return new Error(componentName + ': prop "' + propName + '" is required'); return check(props[propName], propName, componentName); }; required.isRequired = true; required.typeName = typeName; Object.defineProperty(fn, 'isRequired', { get: function() { return required; } }); return fn; } var PropTypes = { string: createChecker(function(v, p, c) { return typeof v !== 'string' ? new Error(c + ': prop "' + p + '" expected string, got ' + typeof v) : null; }, 'string'), number: createChecker(function(v, p, c) { return typeof v !== 'number' ? new Error(c + ': prop "' + p + '" expected number, got ' + typeof v) : null; }, 'number'), bool: createChecker(function(v, p, c) { return typeof v !== 'boolean' ? new Error(c + ': prop "' + p + '" expected boolean, got ' + typeof v) : null; }, 'boolean'), func: createChecker(function(v, p, c) { return typeof v !== 'function' ? new Error(c + ': prop "' + p + '" expected function, got ' + typeof v) : null; }, 'function'), object: createChecker(function(v, p, c) { return typeof v !== 'object' || Array.isArray(v) ? new Error(c + ': prop "' + p + '" expected object') : null; }, 'object'), array: createChecker(function(v, p, c) { return !Array.isArray(v) ? new Error(c + ': prop "' + p + '" expected array, got ' + typeof v) : null; }, 'array'), symbol: createChecker(function(v, p, c) { return typeof v !== 'symbol' ? new Error(c + ': prop "' + p + '" expected symbol') : null; }, 'symbol'), node: createChecker(function(v) { return null; }, 'node'), element: createChecker(function(v, p, c) { return !isValidElement(v) ? new Error(c + ': prop "' + p + '" expected VSV element') : null; }, 'element'), any: createChecker(function() { return null; }, 'any'), arrayOf: function(typeChecker) { return createChecker(function(v, p, c) { if (!Array.isArray(v)) return new Error(c + ': prop "' + p + '" expected array'); for (var i = 0; i < v.length; i++) { var err = typeChecker({ item: v[i] }, 'item', c + '.' + p + '[' + i + ']'); if (err) return err; } return null; }, 'arrayOf'); }, objectOf: function(typeChecker) { return createChecker(function(v, p, c) { if (typeof v !== 'object' || Array.isArray(v)) return new Error(c + ': prop "' + p + '" expected object'); for (var k in v) { var err = typeChecker({ item: v[k] }, 'item', c + '.' + p + '.' + k); if (err) return err; } return null; }, 'objectOf'); }, oneOf: function(values) { return createChecker(function(v, p, c) { return values.indexOf(v) === -1 ? new Error(c + ': prop "' + p + '" expected one of [' + values.join(', ') + '], got ' + v) : null; }, 'oneOf'); }, oneOfType: function(types) { return createChecker(function(v, p, c) { for (var i = 0; i < types.length; i++) { if (!types[i]({ val: v }, 'val', c)) return null; } return new Error(c + ': prop "' + p + '" did not match any of the expected types'); }, 'oneOfType'); }, shape: function(shapeTypes) { return createChecker(function(v, p, c) { if (typeof v !== 'object') return new Error(c + ': prop "' + p + '" expected object'); for (var k in shapeTypes) { var err = shapeTypes[k](v, k, c + '.' + p); if (err) return err; } return null; }, 'shape'); }, exact: function(shapeTypes) { return createChecker(function(v, p, c) { if (typeof v !== 'object') return new Error(c + ': prop "' + p + '" expected object'); var keys = Object.keys(v); var expected = Object.keys(shapeTypes); for (var i = 0; i < keys.length; i++) { if (expected.indexOf(keys[i]) === -1) return new Error(c + ': prop "' + p + '" has unexpected key "' + keys[i] + '"'); } for (var k in shapeTypes) { var err = shapeTypes[k](v, k, c + '.' + p); if (err) return err; } return null; }, 'exact'); }, instanceOf: function(cls) { return createChecker(function(v, p, c) { return !(v instanceof cls) ? new Error(c + ': prop "' + p + '" expected instance of ' + (cls.name || 'Class')) : null; }, 'instanceOf'); }, custom: function(validator) { return createChecker(validator, 'custom'); } }; // Apply defaultProps and validate propTypes function resolveProps(Component, rawProps) { var props = Object.assign({}, rawProps); // Apply defaultProps if (Component.defaultProps) { for (var k in Component.defaultProps) { if (props[k] === undefined) props[k] = Component.defaultProps[k]; } } // Validate propTypes (dev only) if (Component.propTypes) { var name = Component.displayName || Component.name || 'Component'; for (var p in Component.propTypes) { var err = Component.propTypes[p](props, p, name); if (err) console.warn('[VSV PropTypes] ' + err.message); } } return props; } // ======================== // BUILD VSV OBJECT // ======================== var VSV = { version: '2.0.0', // VDOM h: h, Fragment: Fragment, Portal: Portal, // Hooks $state: $state, $effect: $effect, $computed: $computed, $ref: $ref, $memo: $memo, $callback: $callback, $reducer: $reducer, $id: $id, $transition: $transition, $deferred: $deferred, $context: $context, $layoutEffect: $layoutEffect, $imperativeHandle: $imperativeHandle, $debugValue: $debugValue, $syncExternalStore: $syncExternalStore, // Context createContext: createContext, // Router Router: Router, Route: Route, Link: Link, NavLink: NavLink, Redirect: Redirect, navigate: navigate, $location: $location, $params: $params, $query: $query, $navigate: $navigate, // Suspense / Lazy Suspense: Suspense, lazy: lazy, // Error Boundary ErrorBoundary: ErrorBoundary, // Portal createPortal: createPortal, // Ref forwardRef: forwardRef, // Children Children: Children, // Conditional Show: Show, For: For, Switch: Switch, Match: Match, // Head Head: Head, // CSS-in-JS css: css, styled: styled, // Forms $form: $form, // Fetch $fetch: $fetch, // Store createStore: createStore, // Animation $animation: $animation, // Props PropTypes: PropTypes, resolveProps: resolveProps, // Utils cloneElement: cloneElement, isValidElement: isValidElement, memo: memo, // Render render: render, hydrate: hydrate, createElement: createElement, // Registry components: new Map(), instances: new Map(), register: function(name, Component) { this.components.set(name, Component); }, mount: function(Component, container, props) { var vnode = h(Component, props || {}); render(vnode, container); } }; // Expose globally global.VSV = VSV; // Global shortcuts global.$state = $state; global.$effect = $effect; global.$computed = $computed; global.$ref = $ref; global.$memo = $memo; global.$callback = $callback; global.$reducer = $reducer; global.$id = $id; global.$transition = $transition; global.$deferred = $deferred; global.$context = $context; global.$layoutEffect = $layoutEffect; global.$imperativeHandle = $imperativeHandle; global.$form = $form; global.$fetch = $fetch; global.$location = $location; global.$params = $params; global.$query = $query; global.$navigate = $navigate; global.$animation = $animation; // Global component utilities global.PropTypes = PropTypes; global.createContext = createContext; global.createStore = createStore; global.createPortal = createPortal; global.forwardRef = forwardRef; global.memo = memo; global.lazy = lazy; global.css = css; global.styled = styled; global.Children = Children; global.Fragment = Fragment; global.h = h; })(typeof window !== 'undefined' ? window : global); `; } /** * Get minimal runtime (production) */ getMinimalRuntime() { const full = this.getClientRuntime(); return full .replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\/\/.*/g, '') .replace(/\s+/g, ' ') .replace(/\s*([{};,:=()\[\]])\s*/g, '$1') .trim(); } } module.exports = VSVRuntime;